Compare commits

...

46 Commits
1.0.0 ... main

Author SHA1 Message Date
ea608656db
Version 1.3 is ready.
Changelog written, release coming out soon.
2024-05-03 09:08:09 -04:00
a780e58378 Also forgot to update the package info. *Really* done now. 2024-05-01 12:58:08 -04:00
25c660b1b7 Forgot the readme. Version 1.3 should be good now. 2024-05-01 12:50:26 -04:00
861c7c5a03 Zooming now focuses on the mouse cursor. 2024-05-01 12:30:48 -04:00
eea0bba358 Slope field detail changer works. 2024-04-29 07:34:59 -04:00
8b63bf011e Merge remote-tracking branch 'origin/main' into canary 2024-04-26 08:54:11 -04:00
e0a2713585 Updated from other computer. Also, decimal detail slope field works now. 2024-04-24 08:47:23 -04:00
ea9d2cd3b6 Slope field works with decimals now. 2024-04-24 08:42:32 -04:00
789a3b448e Update checker is mostly done. Negative zoom levels are no longer permitted either. 2024-04-21 09:04:35 -04:00
aba32f8b58 Input options now work. I think I can call it done for the zooming system. 2024-04-20 18:02:17 -04:00
ed4ce2bbeb More zoom UI progress. 2024-04-20 09:46:23 -04:00
a6f7279b97 Zoom form finally pretty much ready. 2024-04-17 14:09:34 -04:00
933fcdf082 This took way too long. Zoom-box system works. 2024-04-16 09:43:16 -04:00
3ab55b509a Basic system written. Now I need to add tools for it. Had to delete the SetZoomForm. 2024-04-08 09:56:28 -04:00
41732d01e7 Added UI selection parts. Now the text is more useful. 2024-04-08 09:15:01 -04:00
470a70a97a Some stuff. Satisfied for now with the slope-field generation and the parametric equations. 2024-04-03 12:27:53 -04:00
3b6ebc7b99 Some quick parametrics. See how fast it is to homebrew this? I know I haven't completed a bunch of stuff but I'll get to it soon. 2024-04-02 09:53:42 -04:00
e5c985c060 Improved conversions. You choose whether to remove the original. 2024-04-02 09:37:45 -04:00
d19c7a3fc3 Made a little progress with the update checker. Don't really feel like working on it right now. 2024-04-01 09:41:04 -04:00
4fc2425797 Some updater progress. I want to make it automatic at some point. 2024-03-25 08:50:08 -04:00
3665b0547a Nice progress. Some of it is bodged a bit but that's okay. 2024-03-23 10:33:49 -04:00
66146355c6 Whoopsies. 2024-03-23 09:47:54 -04:00
30529673d0 Offsetter is almost done. 2024-03-23 08:04:41 -04:00
24828e9922 Did tangent line conversion, reworked some of the UI, and started shifting. 2024-03-22 09:56:19 -04:00
a4e9ae3aa5
Version 1.2 is ready. 2024-03-21 12:37:35 -04:00
f8c1788502 About ready for 1.2. 2024-03-20 08:48:23 -04:00
9b4905233c Higher-order integrals are now supported. 2024-03-19 12:34:31 -04:00
f5107b7238 Broke some menu items up into interfaces. Happy with this so far. 2024-03-19 10:41:02 -04:00
855a90b452 Starters on the integral equation. Quite nice so far, I think. 2024-03-19 10:22:00 -04:00
762a5f5a32 Fixed sampling not scaling with DPI. Also disabled implicit usings which is most of this commit. 2024-03-19 08:57:02 -04:00
c30ced7578 Almost done. Added coordinates for selected points. 2024-03-18 14:07:21 -04:00
55cb7af2ac More selection stuff. Do we really want to select tangent lines this way? 2024-03-18 13:14:44 -04:00
8618442bba Forgot this file, my bad. 2024-03-18 09:24:34 -04:00
7ff3720b70 Made most of the methods in virtual. 2024-03-18 09:14:56 -04:00
5c3cf9cafe Made preload functions for most of the other graphables. 2024-03-18 08:57:00 -04:00
255a7d3774 Starting progress on graph selection and preloading. More to come. 2024-03-17 00:45:11 -04:00
f87ef52a7d Added units. Good enough for now. Also increased max zoom. 2024-03-14 14:03:01 -04:00
16f9cb794d Update with main. 2024-03-14 12:43:02 -04:00
cd05a6829c Dynamic line thickness. 2024-03-14 12:42:13 -04:00
a93b5855d0
Merge pull request #1 from That-One-Nerd/canary
Version 1.1 is out.
2024-03-13 10:36:23 -04:00
bd70c17bf6 Whoops, my bad. Now we're ready. 2024-03-13 10:32:05 -04:00
4989de2ed2 Version 1.1 is ready for release. 2024-03-13 10:26:15 -04:00
6d8787cac7 Ready for 1.1. Made a graph cache viewer. 2024-03-13 09:42:03 -04:00
fc829d6a6b Made binary search work with the caching. Much faster, very happy with it. 2024-03-09 16:10:06 -05:00
e31e6bfdb6 Various things. Added a tangent line system, allows for different graph parts, and more caching stuff. 2024-02-29 10:44:28 -05:00
21c498f445 Probably done for today. Added a start to some caching optimizations. 2024-02-27 16:49:06 -05:00
53 changed files with 4085 additions and 439 deletions

1
.gitignore vendored
View File

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

View File

@ -0,0 +1,10 @@
using Graphing.Graphables;
namespace Graphing.Abstract;
public interface IConvertColumnTable
{
public bool UngraphWhenConvertedToColumnTable { get; }
public ColumnTable ToColumnTable(double start, double end, int detail);
}

View File

@ -0,0 +1,10 @@
using Graphing.Graphables;
namespace Graphing.Abstract;
public interface IConvertEquation
{
public bool UngraphWhenConvertedToEquation { get; }
public Equation ToEquation();
}

View File

@ -0,0 +1,10 @@
using Graphing.Graphables;
namespace Graphing.Abstract;
public interface IConvertSlopeField
{
public bool UngraphWhenConvertedToSlopeField { get; }
public SlopeField ToSlopeField(int detail);
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
namespace Graphing.Abstract;
public interface ITranslatable { }

View File

@ -0,0 +1,6 @@
namespace Graphing.Abstract;
public interface ITranslatableX : ITranslatable
{
public double OffsetX { get; set; }
}

View File

@ -0,0 +1,3 @@
namespace Graphing.Abstract;
public interface ITranslatableXY : ITranslatableX, ITranslatableY { }

View File

@ -0,0 +1,6 @@
namespace Graphing.Abstract;
public interface ITranslatableY : ITranslatable
{
public double OffsetY { get; set; }
}

View File

@ -5,23 +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.0.0</Version>
<Version>1.3.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;parametric equation;parametric;difference;tangent</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.3.0</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<Compile Update="Forms\Controls\PieChart.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Forms\GraphColorPickerForm.cs">
<SubType>Form</SubType>
</Compile>
@ -10,5 +16,19 @@
<Compile Update="Forms\SetZoomForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Forms\SlopeFieldDetailForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Forms\TranslateForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Forms\ViewCacheForm.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Forms\ViewCacheForm.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
</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];
}
}

View File

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

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

@ -0,0 +1,46 @@
using System.Drawing;
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 = System.Windows.Forms.AutoScaleMode.Font;
Name = "PieChart";
Size = new Size(500, 500);
ResumeLayout(false);
}
#endregion
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace Graphing.Forms.Controls;
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();
}
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 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)), DpiFloat * 5 / 192);
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

@ -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;

View File

@ -1,4 +1,8 @@
namespace Graphing.Forms;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class GraphColorPickerForm : Form
{
@ -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)
{

View File

@ -1,4 +1,7 @@
namespace Graphing.Forms
using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms
{
partial class GraphForm
{
@ -35,30 +38,45 @@
ButtonViewportSetCenter = new ToolStripMenuItem();
ButtonViewportReset = new ToolStripMenuItem();
ButtonViewportResetWindow = new ToolStripMenuItem();
MenuColors = new ToolStripMenuItem();
MenuEquations = new ToolStripMenuItem();
MenuEquationsDerivative = new ToolStripMenuItem();
MenuEquationsIntegral = new ToolStripMenuItem();
MenuElements = new ToolStripMenuItem();
MenuElementsColors = new ToolStripMenuItem();
MenuElementsRemove = new ToolStripMenuItem();
MenuOperations = new ToolStripMenuItem();
MenuOperationsDerivative = new ToolStripMenuItem();
MenuOperationsIntegral = new ToolStripMenuItem();
MenuOperationsTranslate = new ToolStripMenuItem();
MenuConvert = new ToolStripMenuItem();
MenuConvertEquation = new ToolStripMenuItem();
MenuConvertSlopeField = new ToolStripMenuItem();
MenuMisc = new ToolStripMenuItem();
MenuMiscCaches = new ToolStripMenuItem();
MiscMenuPreload = new ToolStripMenuItem();
UpdaterPopup = new Panel();
UpdaterPopupDownloadButton = new Button();
UpdaterPopupCloseButton = new Button();
UpdaterPopupMessage = new Label();
MenuElementsDetail = new ToolStripMenuItem();
GraphMenu.SuspendLayout();
UpdaterPopup.SuspendLayout();
SuspendLayout();
//
// ResetViewportButton
//
ResetViewportButton.Anchor = AnchorStyles.Top | AnchorStyles.Right;
ResetViewportButton.Font = new Font("Segoe UI Emoji", 13.875F, FontStyle.Regular, GraphicsUnit.Point, 0);
ResetViewportButton.Location = new Point(1373, 43);
ResetViewportButton.Font = new Font("Segoe UI Emoji", 12F, FontStyle.Regular, GraphicsUnit.Point, 0);
ResetViewportButton.Location = new Point(1372, 43);
ResetViewportButton.Margin = new Padding(4, 2, 4, 2);
ResetViewportButton.Name = "ResetViewportButton";
ResetViewportButton.Size = new Size(64, 64);
ResetViewportButton.Size = new Size(63, 64);
ResetViewportButton.TabIndex = 0;
ResetViewportButton.Text = "⌂";
ResetViewportButton.TextAlign = ContentAlignment.TopRight;
ResetViewportButton.Text = "🏠";
ResetViewportButton.UseVisualStyleBackColor = true;
ResetViewportButton.Click += ResetViewportButton_Click;
//
// GraphMenu
//
GraphMenu.ImageScalingSize = new Size(32, 32);
GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuColors, MenuEquations });
GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuElements, MenuOperations, MenuConvert, MenuMisc });
GraphMenu.Location = new Point(0, 0);
GraphMenu.Name = "GraphMenu";
GraphMenu.Size = new Size(1449, 42);
@ -75,68 +93,185 @@
// ButtonViewportSetZoom
//
ButtonViewportSetZoom.Name = "ButtonViewportSetZoom";
ButtonViewportSetZoom.Size = new Size(350, 44);
ButtonViewportSetZoom.Size = new Size(359, 44);
ButtonViewportSetZoom.Text = "Set Zoom";
ButtonViewportSetZoom.Click += ButtonViewportSetZoom_Click;
//
// ButtonViewportSetCenter
//
ButtonViewportSetCenter.Name = "ButtonViewportSetCenter";
ButtonViewportSetCenter.Size = new Size(350, 44);
ButtonViewportSetCenter.Size = new Size(359, 44);
ButtonViewportSetCenter.Text = "Set Center Position";
ButtonViewportSetCenter.Click += ButtonViewportSetCenter_Click;
//
// ButtonViewportReset
//
ButtonViewportReset.Name = "ButtonViewportReset";
ButtonViewportReset.Size = new Size(350, 44);
ButtonViewportReset.Size = new Size(359, 44);
ButtonViewportReset.Text = "Reset Viewport";
ButtonViewportReset.Click += ButtonViewportReset_Click;
//
// ButtonViewportResetWindow
//
ButtonViewportResetWindow.Name = "ButtonViewportResetWindow";
ButtonViewportResetWindow.Size = new Size(350, 44);
ButtonViewportResetWindow.Size = new Size(359, 44);
ButtonViewportResetWindow.Text = "Reset Window Size";
ButtonViewportResetWindow.Click += ButtonViewportResetWindow_Click;
//
// MenuColors
// MenuElements
//
MenuColors.Name = "MenuColors";
MenuColors.Size = new Size(101, 38);
MenuColors.Text = "Colors";
MenuElements.DropDownItems.AddRange(new ToolStripItem[] { MenuElementsColors, MenuElementsDetail, MenuElementsRemove });
MenuElements.Name = "MenuElements";
MenuElements.Size = new Size(131, 38);
MenuElements.Text = "Elements";
//
// MenuEquations
// MenuElementsColors
//
MenuEquations.DropDownItems.AddRange(new ToolStripItem[] { MenuEquationsDerivative, MenuEquationsIntegral });
MenuEquations.Name = "MenuEquations";
MenuEquations.Size = new Size(138, 38);
MenuEquations.Text = "Equations";
MenuElementsColors.Name = "MenuElementsColors";
MenuElementsColors.Size = new Size(359, 44);
MenuElementsColors.Text = "Colors";
//
// MenuEquationsDerivative
// MenuElementsRemove
//
MenuEquationsDerivative.Name = "MenuEquationsDerivative";
MenuEquationsDerivative.Size = new Size(360, 44);
MenuEquationsDerivative.Text = "Compute Derivative";
MenuElementsRemove.Name = "MenuElementsRemove";
MenuElementsRemove.Size = new Size(359, 44);
MenuElementsRemove.Text = "Remove";
//
// MenuEquationsIntegral
// MenuOperations
//
MenuEquationsIntegral.Name = "MenuEquationsIntegral";
MenuEquationsIntegral.Size = new Size(360, 44);
MenuEquationsIntegral.Text = "Compute Integral";
MenuOperations.DropDownItems.AddRange(new ToolStripItem[] { MenuOperationsDerivative, MenuOperationsIntegral, MenuOperationsTranslate });
MenuOperations.Name = "MenuOperations";
MenuOperations.Size = new Size(151, 38);
MenuOperations.Text = "Operations";
//
// MenuOperationsDerivative
//
MenuOperationsDerivative.Name = "MenuOperationsDerivative";
MenuOperationsDerivative.Size = new Size(360, 44);
MenuOperationsDerivative.Text = "Compute Derivative";
//
// MenuOperationsIntegral
//
MenuOperationsIntegral.Name = "MenuOperationsIntegral";
MenuOperationsIntegral.Size = new Size(360, 44);
MenuOperationsIntegral.Text = "Compute Integral";
//
// MenuOperationsTranslate
//
MenuOperationsTranslate.Name = "MenuOperationsTranslate";
MenuOperationsTranslate.Size = new Size(360, 44);
MenuOperationsTranslate.Text = "Translate";
//
// MenuConvert
//
MenuConvert.DropDownItems.AddRange(new ToolStripItem[] { MenuConvertEquation, MenuConvertSlopeField });
MenuConvert.Name = "MenuConvert";
MenuConvert.Size = new Size(118, 38);
MenuConvert.Text = "Convert";
//
// MenuConvertEquation
//
MenuConvertEquation.Name = "MenuConvertEquation";
MenuConvertEquation.Size = new Size(297, 44);
MenuConvertEquation.Text = "To Equation";
//
// MenuConvertSlopeField
//
MenuConvertSlopeField.Name = "MenuConvertSlopeField";
MenuConvertSlopeField.Size = new Size(297, 44);
MenuConvertSlopeField.Text = "To Slope Field";
//
// MenuMisc
//
MenuMisc.DropDownItems.AddRange(new ToolStripItem[] { MenuMiscCaches, MiscMenuPreload });
MenuMisc.Name = "MenuMisc";
MenuMisc.Size = new Size(83, 38);
MenuMisc.Text = "Misc";
//
// MenuMiscCaches
//
MenuMiscCaches.Name = "MenuMiscCaches";
MenuMiscCaches.Size = new Size(299, 44);
MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click;
//
// MiscMenuPreload
//
MiscMenuPreload.Name = "MiscMenuPreload";
MiscMenuPreload.Size = new Size(299, 44);
MiscMenuPreload.Text = "Preload Cache";
MiscMenuPreload.Click += MiscMenuPreload_Click;
//
// UpdaterPopup
//
UpdaterPopup.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
UpdaterPopup.BackColor = SystemColors.HighlightText;
UpdaterPopup.BorderStyle = BorderStyle.FixedSingle;
UpdaterPopup.Controls.Add(UpdaterPopupDownloadButton);
UpdaterPopup.Controls.Add(UpdaterPopupCloseButton);
UpdaterPopup.Controls.Add(UpdaterPopupMessage);
UpdaterPopup.Location = new Point(966, 791);
UpdaterPopup.Margin = new Padding(6, 6, 6, 6);
UpdaterPopup.Name = "UpdaterPopup";
UpdaterPopup.Size = new Size(483, 115);
UpdaterPopup.TabIndex = 2;
UpdaterPopup.Visible = false;
//
// UpdaterPopupDownloadButton
//
UpdaterPopupDownloadButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
UpdaterPopupDownloadButton.Location = new Point(336, 58);
UpdaterPopupDownloadButton.Margin = new Padding(6, 6, 6, 6);
UpdaterPopupDownloadButton.Name = "UpdaterPopupDownloadButton";
UpdaterPopupDownloadButton.Size = new Size(139, 49);
UpdaterPopupDownloadButton.TabIndex = 2;
UpdaterPopupDownloadButton.Text = "Visit";
UpdaterPopupDownloadButton.UseVisualStyleBackColor = true;
//
// UpdaterPopupCloseButton
//
UpdaterPopupCloseButton.Anchor = AnchorStyles.Top | AnchorStyles.Right;
UpdaterPopupCloseButton.Location = new Point(435, 2);
UpdaterPopupCloseButton.Margin = new Padding(2, 2, 2, 2);
UpdaterPopupCloseButton.Name = "UpdaterPopupCloseButton";
UpdaterPopupCloseButton.Size = new Size(45, 51);
UpdaterPopupCloseButton.TabIndex = 1;
UpdaterPopupCloseButton.Text = "X";
UpdaterPopupCloseButton.UseVisualStyleBackColor = true;
UpdaterPopupCloseButton.Click += UpdaterPopupCloseButton_Click;
//
// UpdaterPopupMessage
//
UpdaterPopupMessage.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left;
UpdaterPopupMessage.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0);
UpdaterPopupMessage.Location = new Point(6, 6);
UpdaterPopupMessage.Margin = new Padding(6, 6, 6, 6);
UpdaterPopupMessage.Name = "UpdaterPopupMessage";
UpdaterPopupMessage.Size = new Size(423, 100);
UpdaterPopupMessage.TabIndex = 0;
UpdaterPopupMessage.Text = "A <type> update is available!\r\nA.B.C → E.F.G";
//
// MenuElementsDetail
//
MenuElementsDetail.Name = "MenuElementsDetail";
MenuElementsDetail.Size = new Size(359, 44);
MenuElementsDetail.Text = "Detail";
//
// GraphForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1449, 907);
Controls.Add(UpdaterPopup);
Controls.Add(ResetViewportButton);
Controls.Add(GraphMenu);
MainMenuStrip = GraphMenu;
Margin = new Padding(4, 2, 4, 2);
Name = "GraphForm";
Text = "GraphFormBase";
GraphMenu.ResumeLayout(false);
GraphMenu.PerformLayout();
UpdaterPopup.ResumeLayout(false);
ResumeLayout(false);
PerformLayout();
}
@ -145,14 +280,28 @@
private Button ResetViewportButton;
private MenuStrip GraphMenu;
private ToolStripMenuItem MenuColors;
private ToolStripMenuItem MenuViewport;
private ToolStripMenuItem ButtonViewportSetZoom;
private ToolStripMenuItem ButtonViewportSetCenter;
private ToolStripMenuItem ButtonViewportReset;
private ToolStripMenuItem ButtonViewportResetWindow;
private ToolStripMenuItem MenuEquations;
private ToolStripMenuItem MenuEquationsDerivative;
private ToolStripMenuItem MenuEquationsIntegral;
private ToolStripMenuItem MenuOperations;
private ToolStripMenuItem MenuOperationsDerivative;
private ToolStripMenuItem MenuOperationsIntegral;
private ToolStripMenuItem MenuMisc;
private ToolStripMenuItem MenuMiscCaches;
private ToolStripMenuItem MiscMenuPreload;
private ToolStripMenuItem MenuConvert;
private ToolStripMenuItem MenuConvertEquation;
private ToolStripMenuItem MenuElements;
private ToolStripMenuItem MenuElementsColors;
private ToolStripMenuItem MenuElementsRemove;
private ToolStripMenuItem MenuOperationsTranslate;
private ToolStripMenuItem MenuConvertSlopeField;
private Panel UpdaterPopup;
private Label UpdaterPopupMessage;
private Button UpdaterPopupCloseButton;
private Button UpdaterPopupDownloadButton;
private ToolStripMenuItem MenuElementsDetail;
}
}

View File

@ -1,32 +1,71 @@
using Graphing.Graphables;
using Graphing.Abstract;
using Graphing.Graphables;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class GraphForm : Form
{
public Float2 ScreenCenter { get; private set; }
public static readonly Color BackgroundColor = Color.White;
public static readonly Color MainAxisColor = Color.Black;
public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); // Grayish
public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0)); // Lighter grayish
public static readonly Color UnitsTextColor = Color.Black;
public static readonly Color ZoomBoxColor = Color.Black;
public static readonly Color MajorUpdateColor = Color.FromArgb(unchecked((int)0xFF_F74434)); // Red
public static readonly Color MinorUpdateColor = Color.FromArgb(unchecked((int)0xFF_FCA103)); // Orange
public Float2 ScreenCenter { get; set; }
public Float2 Dpi { get; private set; }
public double ZoomLevel
public float DpiFloat { get; private set; }
public Float2 ZoomLevel
{
get => _zoomLevel;
set
{
double oldZoom = ZoomLevel;
_zoomLevel = new(Math.Clamp(value.x, 1e-5, 1e3),
Math.Clamp(value.y, 1e-5, 1e3));
OnZoomLevelChanged(this, new());
Invalidate(false);
}
}
private Float2 _zoomLevel;
_zoomLevel = Math.Clamp(value, 1e-2, 1e3);
int totalSegments = 0;
foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count();
if (totalSegments > 10_000)
public bool ViewportLocked
{
_zoomLevel = oldZoom;
return; // Too many segments, stop.
get => _viewportLocked;
set
{
if (value)
{
FormBorderStyle = FormBorderStyle.FixedSingle;
ResetViewportButton.Text = "🔒";
}
else
{
FormBorderStyle = FormBorderStyle.Sizable;
ResetViewportButton.Text = "🏠";
}
MaximizeBox = !value;
ResetViewportButton.Enabled = !value;
_viewportLocked = value;
}
}
}
private double _zoomLevel;
private bool _viewportLocked;
private readonly Point initialWindowPos;
private readonly Size initialWindowSize;
@ -38,6 +77,8 @@ public partial class GraphForm : Form
private readonly List<Graphable> ables;
public event EventHandler OnZoomLevelChanged = delegate { };
public GraphForm(string title)
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
@ -50,10 +91,15 @@ 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;
ZoomLevel = new(1, 1);
initialWindowPos = Location;
initialWindowSize = Size;
RunUpdateChecker();
}
public Int2 GraphSpaceToScreenSpace(Float2 graphPoint)
@ -63,8 +109,8 @@ public partial class GraphForm : Form
graphPoint.x -= ScreenCenter.x;
graphPoint.y -= ScreenCenter.y;
graphPoint.x *= Dpi.x / ZoomLevel;
graphPoint.y *= Dpi.y / ZoomLevel;
graphPoint.x *= Dpi.x / ZoomLevel.x;
graphPoint.y *= Dpi.y / ZoomLevel.y;
graphPoint.x += ClientRectangle.Width / 2.0;
graphPoint.y += ClientRectangle.Height / 2.0;
@ -78,8 +124,8 @@ public partial class GraphForm : Form
result.x -= ClientRectangle.Width / 2.0;
result.y -= ClientRectangle.Height / 2.0;
result.x /= Dpi.x / ZoomLevel;
result.y /= Dpi.y / ZoomLevel;
result.x /= Dpi.x / ZoomLevel.x;
result.y /= Dpi.y / ZoomLevel.y;
result.x += ScreenCenter.x;
result.y += ScreenCenter.y;
@ -91,19 +137,20 @@ public partial class GraphForm : Form
protected virtual void PaintGrid(Graphics g)
{
double axisScale = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel)));
double axisScaleX = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.x))),
axisScaleY = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.y)));
// Draw horizontal/vertical quarter-axis.
Brush quarterBrush = new SolidBrush(Color.FromArgb(unchecked((int)0xFF_E0E0E0)));
Pen quarterPen = new(quarterBrush, 2);
Brush quarterBrush = new SolidBrush(QuarterAxisColor);
Pen quarterPen = new(quarterBrush, DpiFloat * 2 / 192);
for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScale) * axisScale / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScale) * axisScale / 4; x += axisScale / 4)
for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScaleX) * axisScaleX / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScaleX) * axisScaleX / 4; x += axisScaleX / 4)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)),
endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y));
g.DrawLine(quarterPen, startPos, endPos);
}
for (double y = Math.Ceiling(MinVisibleGraph.y * 4 / axisScale) * axisScale / 4; y <= Math.Floor(MaxVisibleGraph.y * 4 / axisScale) * axisScale / 4; y += axisScale / 4)
for (double y = Math.Ceiling(MinVisibleGraph.y * 4 / axisScaleY) * axisScaleY / 4; y <= Math.Floor(MaxVisibleGraph.y * 4 / axisScaleY) * axisScaleY / 4; y += axisScaleY / 4)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)),
endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y));
@ -111,24 +158,24 @@ public partial class GraphForm : Form
}
// Draw horizontal/vertical semi-axis.
Brush semiBrush = new SolidBrush(Color.FromArgb(unchecked((int)0xFF_999999)));
Pen semiPen = new(semiBrush, 2);
Brush semiBrush = new SolidBrush(SemiAxisColor);
Pen semiPen = new(semiBrush, DpiFloat * 2 / 192);
for (double x = Math.Ceiling(MinVisibleGraph.x / axisScale) * axisScale; x <= Math.Floor(MaxVisibleGraph.x / axisScale) * axisScale; x += axisScale)
for (double x = Math.Ceiling(MinVisibleGraph.x / axisScaleX) * axisScaleX; x <= Math.Floor(MaxVisibleGraph.x / axisScaleX) * axisScaleX; x += axisScaleX)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)),
endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y));
g.DrawLine(semiPen, startPos, endPos);
}
for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= Math.Floor(MaxVisibleGraph.y / axisScale) * axisScale; y += axisScale)
for (double y = Math.Ceiling(MinVisibleGraph.y / axisScaleY) * axisScaleY; y <= Math.Floor(MaxVisibleGraph.y / axisScaleY) * axisScaleY; y += axisScaleY)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)),
endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y));
g.DrawLine(semiPen, startPos, endPos);
}
Brush mainLineBrush = new SolidBrush(Color.Black);
Pen mainLinePen = new(mainLineBrush, 3);
Brush mainLineBrush = new SolidBrush(MainAxisColor);
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)),
@ -139,32 +186,98 @@ public partial class GraphForm : Form
g.DrawLine(mainLinePen, startCenterX, endCenterX);
g.DrawLine(mainLinePen, startCenterY, endCenterY);
}
protected virtual void PaintUnits(Graphics g)
{
double axisScaleX = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.x))),
axisScaleY = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.y)));
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 / axisScaleX) * axisScaleX; x <= MaxVisibleGraph.x; x += axisScaleX)
{
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 / axisScaleY) * axisScaleY; y <= MaxVisibleGraph.y; y += axisScaleY)
{
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)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
Brush background = new SolidBrush(Color.White);
Brush background = new SolidBrush(BackgroundColor);
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<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);
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 (selectState == SelectionState.GraphSelect)
{
for (int i = 0; i < ables.Count; i++)
{
if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5))
{
IEnumerable<IGraphPart> selectionParts = ables[i].GetSelectionItemsToRender(this, graphMousePos);
foreach (IGraphPart selPart in selectionParts) selPart.Render(this, g, graphPens[i]);
}
}
}
else if (selectState == SelectionState.ZoomBox)
{
// Draw the current box selection.
Int2 boxPosA = GraphSpaceToScreenSpace(boxSelectA),
boxPosB = GraphSpaceToScreenSpace(boxSelectB);
if (boxPosA.x > boxPosB.x) (boxPosA.x, boxPosB.x) = (boxPosB.x, boxPosA.x);
if (boxPosA.y > boxPosB.y) (boxPosA.y, boxPosB.y) = (boxPosB.y, boxPosA.y);
Pen boxPen = new(ZoomBoxColor, 2 * DpiFloat / 192);
g.DrawRectangle(boxPen, new(boxPosA.x, boxPosA.y,
boxPosB.x - boxPosA.x,
boxPosB.y - boxPosA.y));
}
base.OnPaint(e);
@ -175,60 +288,153 @@ public partial class GraphForm : Form
Invalidate(false);
}
public void Graph(Graphable able)
public void Graph(params Graphable[] newAbles)
{
ables.Add(able);
ables.AddRange(newAbles);
RegenerateMenuItems();
Invalidate(false);
}
public void Ungraph(params Graphable[] ables)
{
this.ables.RemoveAll(x => ables.Contains(x));
RegenerateMenuItems();
Invalidate(false);
}
private bool mouseDrag = false;
public bool IsGraphPointVisible(Float2 point)
{
Int2 pixelPos = GraphSpaceToScreenSpace(point);
return pixelPos.x >= 0 && pixelPos.x < ClientRectangle.Width &&
pixelPos.y >= 0 && pixelPos.y < ClientRectangle.Height;
}
private SelectionState selectState = SelectionState.None;
internal bool canBoxSelect;
private SetZoomForm? setZoomForm;
private Int2 initialMouseLocation;
private Float2 initialScreenCenter;
private Float2 boxSelectA, boxSelectB;
protected override void OnMouseDown(MouseEventArgs e)
{
mouseDrag = true;
if (selectState == SelectionState.None && canBoxSelect)
{
Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y));
boxSelectA = graphMousePos;
boxSelectB = graphMousePos;
selectState = SelectionState.ZoomBox;
}
if (selectState == SelectionState.None)
{
Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y));
foreach (Graphable able in Graphables)
{
if (able.ShouldSelectGraphable(this, graphMousePos, 1))
selectState = SelectionState.GraphSelect;
}
if (selectState == SelectionState.GraphSelect) Invalidate(false);
}
if (selectState == SelectionState.None && !ViewportLocked)
{
selectState = SelectionState.ViewportDrag;
initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y);
initialScreenCenter = ScreenCenter;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
if (mouseDrag)
if (selectState == SelectionState.None) return;
else if (selectState == SelectionState.ViewportDrag)
{
Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X,
initialMouseLocation.y - Cursor.Position.Y);
Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y);
Float2 graphDiff = new(pixelDiff.x * ZoomLevel.x / Dpi.x, pixelDiff.y * ZoomLevel.y / Dpi.y);
ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y);
Invalidate(false);
}
mouseDrag = false;
else if (selectState == SelectionState.ZoomBox)
{
Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y));
boxSelectB = graphMousePos;
// Set center.
ScreenCenter = new((boxSelectA.x + boxSelectB.x) * 0.5,
-(boxSelectA.y + boxSelectB.y) * 0.5);
// Set zoom. Kind of weird but it works.
Float2 minGraph = MinVisibleGraph, maxGraph = MaxVisibleGraph;
Float2 oldDist = new(maxGraph.x - minGraph.x,
maxGraph.y - minGraph.y);
Float2 newDist = new(Math.Abs(boxSelectB.x - boxSelectA.x),
Math.Abs(boxSelectB.y - boxSelectA.y));
ZoomLevel = new(ZoomLevel.x * newDist.x / oldDist.x,
ZoomLevel.y * newDist.y / oldDist.y);
setZoomForm!.CompleteBoxSelection();
boxSelectA = new(0, 0);
boxSelectB = new(0, 0);
}
selectState = SelectionState.None;
Invalidate(false);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (mouseDrag)
if (selectState == SelectionState.None) return;
else if (selectState == SelectionState.ViewportDrag)
{
Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X,
initialMouseLocation.y - Cursor.Position.Y);
Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y);
Float2 graphDiff = new(pixelDiff.x * ZoomLevel.x / Dpi.x, pixelDiff.y * ZoomLevel.y / Dpi.y);
ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y);
Invalidate(false);
}
else if (selectState == SelectionState.ZoomBox)
{
Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y));
boxSelectB = graphMousePos;
}
Invalidate(false);
}
protected override void OnMouseWheel(MouseEventArgs e)
{
ZoomLevel *= 1 - e.Delta * 0.00075; // Zoom factor.
if (ViewportLocked) return;
Point clientMousePos = PointToClient(Cursor.Position);
Int2 mousePos = new(clientMousePos.X, clientMousePos.Y);
Float2 mouseOver = ScreenSpaceToGraphSpace(mousePos);
Float2 newZoom = ZoomLevel;
newZoom.x *= 1 - e.Delta * 0.00075; // Zoom factor.
newZoom.y *= 1 - e.Delta * 0.00075;
ZoomLevel = newZoom;
// Keep the mouse as the zoom hotspot.
Float2 newOver = ScreenSpaceToGraphSpace(mousePos);
Float2 delta = new(newOver.x - mouseOver.x, newOver.y - mouseOver.y);
ScreenCenter = new(ScreenCenter.x - delta.x, ScreenCenter.y + delta.y);
Invalidate(false);
}
private void ResetViewportButton_Click(object? sender, EventArgs e)
{
ScreenCenter = new Float2(0, 0);
ZoomLevel = 1;
Invalidate(false);
ResetAllViewport();
}
private void GraphColorPickerButton_Click(Graphable able)
{
GraphColorPickerForm picker = new(this, able)
@ -237,15 +443,57 @@ 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();
}
private readonly Dictionary<SlopeField, SlopeFieldDetailForm> sfDetailForms = [];
private void ChangeSlopeFieldDetail(SlopeField sf)
{
if (sfDetailForms.TryGetValue(sf, out SlopeFieldDetailForm? preexistingForm))
{
preexistingForm.Focus();
return;
}
SlopeFieldDetailForm detailForm = new(this, sf)
{
StartPosition = FormStartPosition.Manual
};
sfDetailForms.Add(sf, detailForm);
detailForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - detailForm.ClientRectangle.Height) / 2);
if (detailForm.Location.X + detailForm.Width > Screen.FromControl(this).WorkingArea.Width)
{
detailForm.StartPosition = FormStartPosition.WindowsDefaultLocation;
}
detailForm.TopMost = true;
detailForm.Show();
detailForm.FormClosed += (o, e) => sfDetailForms.Remove(sf);
}
private void RegenerateMenuItems()
{
MenuColors.DropDownItems.Clear();
MenuEquationsDerivative.DropDownItems.Clear();
MenuEquationsIntegral.DropDownItems.Clear();
MenuElementsColors.DropDownItems.Clear();
MenuElementsDetail.DropDownItems.Clear();
MenuElementsRemove.DropDownItems.Clear();
MenuOperationsDerivative.DropDownItems.Clear();
MenuOperationsIntegral.DropDownItems.Clear();
MenuConvertEquation.DropDownItems.Clear();
MenuConvertSlopeField.DropDownItems.Clear();
MenuOperationsTranslate.DropDownItems.Clear();
// At some point, we'll have a Convert To Column Table button,
// but I'll need to make a form for the ranges when I do that.
foreach (Graphable able in ables)
{
@ -255,52 +503,126 @@ public partial class GraphForm : Form
Text = able.Name
};
colorItem.Click += (o, e) => GraphColorPickerButton_Click(able);
MenuColors.DropDownItems.Add(colorItem);
MenuElementsColors.DropDownItems.Add(colorItem);
if (able is Equation)
ToolStripMenuItem removeItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
removeItem.Click += (o, e) => Ungraph(able);
MenuElementsRemove.DropDownItems.Add(removeItem);
if (able is SlopeField sf)
{
ToolStripMenuItem sfDetailItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
sfDetailItem.Click += (o, e) => ChangeSlopeFieldDetail(sf);
MenuElementsDetail.DropDownItems.Add(sfDetailItem);
}
if (able is IDerivable derivable)
{
ToolStripMenuItem derivativeItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
derivativeItem.Click += (o, e) => EquationComputeDerivative_Click((able as Equation)!);
MenuEquationsDerivative.DropDownItems.Add(derivativeItem);
derivativeItem.Click += (o, e) => Graph(derivable.Derive());
MenuOperationsDerivative.DropDownItems.Add(derivativeItem);
}
if (able is IIntegrable integrable)
{
ToolStripMenuItem integralItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
integralItem.Click += (o, e) => EquationComputeIntegral_Click((able as Equation)!);
MenuEquationsIntegral.DropDownItems.Add(integralItem);
integralItem.Click += (o, e) => Graph(integrable.Integrate());
MenuOperationsIntegral.DropDownItems.Add(integralItem);
}
if (able is IConvertEquation equConvert)
{
ToolStripMenuItem equItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
equItem.Click += (o, e) =>
{
if (equConvert.UngraphWhenConvertedToEquation) Ungraph(able);
Graph(equConvert.ToEquation());
};
MenuConvertEquation.DropDownItems.Add(equItem);
}
if (able is IConvertSlopeField sfConvert)
{
ToolStripMenuItem sfItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
sfItem.Click += (o, e) =>
{
if (sfConvert.UngraphWhenConvertedToSlopeField) Ungraph(able);
Graph(sfConvert.ToSlopeField(2));
};
MenuConvertSlopeField.DropDownItems.Add(sfItem);
}
if (able is ITranslatable translatable)
{
ToolStripMenuItem transItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
transItem.Click += (o, e) => ElementsOperationsTranslate_Click(able, translatable);
MenuOperationsTranslate.DropDownItems.Add(transItem);
}
}
}
private void ButtonViewportSetZoom_Click(object? sender, EventArgs e)
{
SetZoomForm picker = new(this)
if (setZoomForm is not null)
{
setZoomForm.Focus();
return;
}
SetZoomForm zoomForm = new(this)
{
StartPosition = FormStartPosition.Manual,
};
picker.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2);
picker.ShowDialog();
zoomForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - zoomForm.ClientRectangle.Height) / 2);
if (zoomForm.Location.X + zoomForm.Width > Screen.FromControl(this).WorkingArea.Width)
{
zoomForm.StartPosition = FormStartPosition.WindowsDefaultLocation;
}
setZoomForm = zoomForm;
zoomForm.Show();
zoomForm.FormClosing += (o, e) =>
{
zoomForm.CompleteBoxSelection();
setZoomForm = null;
};
}
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;
ZoomLevel = new(1, 1);
Invalidate(false);
}
private void ButtonViewportResetWindow_Click(object? sender, EventArgs e)
{
Location = initialWindowPos;
@ -308,60 +630,147 @@ public partial class GraphForm : Form
WindowState = FormWindowState.Normal;
}
private void EquationComputeDerivative_Click(Equation equation)
public void ResetAllViewport()
{
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).
ScreenCenter = new Float2(0, 0);
ZoomLevel = new(1, 1);
Location = initialWindowPos;
Size = initialWindowSize;
WindowState = FormWindowState.Normal;
Invalidate(false);
}
Graph(new Equation(DerivativeAtPoint(equ))
private ViewCacheForm? cacheForm;
private void MenuMiscCaches_Click(object? sender, EventArgs e)
{
Name = newName
});
if (this.cacheForm is not null)
{
this.cacheForm.Focus();
return;
}
static EquationDelegate DerivativeAtPoint(EquationDelegate e)
ViewCacheForm cacheForm = new(this)
{
const double step = 1e-3;
return x => (e(x + step) - e(x)) / step;
StartPosition = FormStartPosition.Manual
};
this.cacheForm = cacheForm;
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);
}
private void UpdaterPopupCloseButton_Click(object? sender, EventArgs e)
{
UpdaterPopup.Dispose();
}
private void ElementsOperationsTranslate_Click(Graphable ableRaw, ITranslatable ableTrans)
{
TranslateForm shifter = new(this, ableRaw, ableTrans)
{
StartPosition = FormStartPosition.Manual,
};
shifter.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - shifter.ClientRectangle.Height) / 2);
if (shifter.Location.X + shifter.Width > Screen.FromControl(this).WorkingArea.Width)
{
shifter.StartPosition = FormStartPosition.WindowsDefaultLocation;
}
shifter.Show();
}
private async void RunUpdateChecker()
{
try
{
HttpClient http = new();
HttpRequestMessage request = new(HttpMethod.Get, "https://api.github.com/repos/That-One-Nerd/Graphing/releases");
request.Headers.Add("User-Agent", "ThatOneNerd.Graphing-Update-Checker");
HttpResponseMessage result = await http.SendAsync(request);
if (!result.IsSuccessStatusCode)
{
Console.WriteLine($"Failed to check for updates.");
return;
}
JsonArray arr = JsonSerializer.Deserialize<JsonArray>(await result.Content.ReadAsStreamAsync())!;
JsonObject latest = arr[0]!.AsObject();
Version curVersion = Version.Parse(Assembly.GetAssembly(typeof(GraphForm))!.FullName!.Split(',')[1].Trim()[8..^2]);
Version newVersion = Version.Parse(latest["tag_name"]!.GetValue<string>());
if (newVersion > curVersion)
{
string type;
if (newVersion.Major > curVersion.Major || // x.0.0
newVersion.Minor > curVersion.Minor) // 0.x.0
{
type = "major";
UpdaterPopupMessage.ForeColor = MajorUpdateColor;
}
else // 0.0.x
{
type = "minor";
UpdaterPopupMessage.ForeColor = MinorUpdateColor;
}
UpdaterPopupMessage.Text = $"A {type} update is available!\n{curVersion} → {newVersion}";
UpdaterPopup.Visible = true;
string url = latest["html_url"]!.GetValue<string>();
Console.WriteLine($"An update is available! {curVersion} -> {newVersion}\n{url}");
UpdaterPopupDownloadButton.Click += (o, e) =>
{
ProcessStartInfo website = new()
{
FileName = url,
UseShellExecute = true
};
Process.Start(website);
};
}
else
{
Console.WriteLine("Up-to-date.");
UpdaterPopup.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to check for updates:\n{ex}");
}
}
private void EquationComputeIntegral_Click(Equation equation)
private enum SelectionState
{
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-1;
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;
}
None = 0,
ViewportDrag,
GraphSelect,
ZoomBox,
}
}

View File

@ -28,90 +28,157 @@
/// </summary>
private void InitializeComponent()
{
MessageLabel = new Label();
ZoomTrackBar = new TrackBar();
ValueLabel = new Label();
ZoomMinValue = new TextBox();
ZoomMaxValue = new TextBox();
((System.ComponentModel.ISupportInitialize)ZoomTrackBar).BeginInit();
EnableBoxSelect = new System.Windows.Forms.Button();
MatchAspectButton = new System.Windows.Forms.Button();
ResetButton = new System.Windows.Forms.Button();
NormalizeButton = new System.Windows.Forms.Button();
MinBoxX = new System.Windows.Forms.TextBox();
TextX = new System.Windows.Forms.Label();
MaxBoxX = new System.Windows.Forms.TextBox();
MaxBoxY = new System.Windows.Forms.TextBox();
TextY = new System.Windows.Forms.Label();
MinBoxY = new System.Windows.Forms.TextBox();
ViewportLock = new System.Windows.Forms.CheckBox();
SuspendLayout();
//
// MessageLabel
// EnableBoxSelect
//
MessageLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
MessageLabel.Location = new Point(52, 20);
MessageLabel.Name = "MessageLabel";
MessageLabel.Size = new Size(413, 35);
MessageLabel.TabIndex = 0;
MessageLabel.Text = "Set the zoom level for the graph.";
MessageLabel.TextAlign = ContentAlignment.MiddleCenter;
EnableBoxSelect.Location = new System.Drawing.Point(12, 12);
EnableBoxSelect.Name = "EnableBoxSelect";
EnableBoxSelect.Size = new System.Drawing.Size(187, 46);
EnableBoxSelect.TabIndex = 0;
EnableBoxSelect.Text = "Box Select";
EnableBoxSelect.UseVisualStyleBackColor = true;
EnableBoxSelect.Click += EnableBoxSelect_Click;
//
// ZoomTrackBar
// MatchAspectButton
//
ZoomTrackBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
ZoomTrackBar.LargeChange = 1000;
ZoomTrackBar.Location = new Point(12, 127);
ZoomTrackBar.Maximum = 10000;
ZoomTrackBar.Name = "ZoomTrackBar";
ZoomTrackBar.Size = new Size(489, 90);
ZoomTrackBar.TabIndex = 1;
ZoomTrackBar.TickStyle = TickStyle.None;
ZoomTrackBar.Scroll += ZoomTrackBar_Scroll;
MatchAspectButton.Location = new System.Drawing.Point(12, 64);
MatchAspectButton.Name = "MatchAspectButton";
MatchAspectButton.Size = new System.Drawing.Size(187, 46);
MatchAspectButton.TabIndex = 1;
MatchAspectButton.Text = "Match Aspect";
MatchAspectButton.UseVisualStyleBackColor = true;
MatchAspectButton.Click += MatchAspectButton_Click;
//
// ValueLabel
// ResetButton
//
ValueLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
ValueLabel.Location = new Point(52, 91);
ValueLabel.Name = "ValueLabel";
ValueLabel.Size = new Size(413, 33);
ValueLabel.TabIndex = 2;
ValueLabel.Text = "1.00x";
ValueLabel.TextAlign = ContentAlignment.TopCenter;
ResetButton.Location = new System.Drawing.Point(12, 168);
ResetButton.Name = "ResetButton";
ResetButton.Size = new System.Drawing.Size(187, 46);
ResetButton.TabIndex = 2;
ResetButton.Text = "Reset";
ResetButton.UseVisualStyleBackColor = true;
ResetButton.Click += ResetButton_Click;
//
// ZoomMinValue
// NormalizeButton
//
ZoomMinValue.Location = new Point(12, 178);
ZoomMinValue.Name = "ZoomMinValue";
ZoomMinValue.Size = new Size(83, 39);
ZoomMinValue.TabIndex = 3;
ZoomMinValue.Text = "0.50";
ZoomMinValue.TextChanged += ZoomMinValue_TextChanged;
NormalizeButton.Location = new System.Drawing.Point(12, 116);
NormalizeButton.Name = "NormalizeButton";
NormalizeButton.Size = new System.Drawing.Size(187, 46);
NormalizeButton.TabIndex = 3;
NormalizeButton.Text = "Normalize";
NormalizeButton.UseVisualStyleBackColor = true;
NormalizeButton.Click += NormalizeButton_Click;
//
// ZoomMaxValue
// MinBoxX
//
ZoomMaxValue.Anchor = AnchorStyles.Top | AnchorStyles.Right;
ZoomMaxValue.Location = new Point(418, 178);
ZoomMaxValue.Name = "ZoomMaxValue";
ZoomMaxValue.Size = new Size(83, 39);
ZoomMaxValue.TabIndex = 4;
ZoomMaxValue.Text = "2.00";
ZoomMaxValue.TextAlign = HorizontalAlignment.Right;
ZoomMaxValue.TextChanged += ZoomMaxValue_TextChanged;
MinBoxX.Location = new System.Drawing.Point(227, 49);
MinBoxX.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3);
MinBoxX.Name = "MinBoxX";
MinBoxX.Size = new System.Drawing.Size(108, 39);
MinBoxX.TabIndex = 4;
//
// TextX
//
TextX.Location = new System.Drawing.Point(335, 49);
TextX.Margin = new System.Windows.Forms.Padding(0);
TextX.Name = "TextX";
TextX.Size = new System.Drawing.Size(77, 39);
TextX.TabIndex = 5;
TextX.Text = "≤ x ≤";
TextX.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// MaxBoxX
//
MaxBoxX.Location = new System.Drawing.Point(412, 49);
MaxBoxX.Margin = new System.Windows.Forms.Padding(0, 3, 25, 3);
MaxBoxX.Name = "MaxBoxX";
MaxBoxX.Size = new System.Drawing.Size(108, 39);
MaxBoxX.TabIndex = 6;
//
// MaxBoxY
//
MaxBoxY.Location = new System.Drawing.Point(412, 94);
MaxBoxY.Margin = new System.Windows.Forms.Padding(0, 3, 25, 3);
MaxBoxY.Name = "MaxBoxY";
MaxBoxY.Size = new System.Drawing.Size(108, 39);
MaxBoxY.TabIndex = 9;
//
// TextY
//
TextY.Location = new System.Drawing.Point(335, 94);
TextY.Margin = new System.Windows.Forms.Padding(0);
TextY.Name = "TextY";
TextY.Size = new System.Drawing.Size(77, 39);
TextY.TabIndex = 8;
TextY.Text = "≤ y ≤";
TextY.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// MinBoxY
//
MinBoxY.Location = new System.Drawing.Point(227, 94);
MinBoxY.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3);
MinBoxY.Name = "MinBoxY";
MinBoxY.Size = new System.Drawing.Size(108, 39);
MinBoxY.TabIndex = 7;
//
// ViewportLock
//
ViewportLock.Location = new System.Drawing.Point(227, 139);
ViewportLock.Name = "ViewportLock";
ViewportLock.Size = new System.Drawing.Size(293, 39);
ViewportLock.TabIndex = 10;
ViewportLock.Text = "Lock Viewport";
ViewportLock.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
ViewportLock.UseVisualStyleBackColor = true;
ViewportLock.CheckedChanged += ViewportLock_CheckedChanged;
//
// SetZoomForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(513, 230);
Controls.Add(ZoomMaxValue);
Controls.Add(ZoomMinValue);
Controls.Add(ValueLabel);
Controls.Add(ZoomTrackBar);
Controls.Add(MessageLabel);
FormBorderStyle = FormBorderStyle.FixedToolWindow;
AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(533, 227);
Controls.Add(ViewportLock);
Controls.Add(MaxBoxY);
Controls.Add(TextY);
Controls.Add(MinBoxY);
Controls.Add(MaxBoxX);
Controls.Add(TextX);
Controls.Add(MinBoxX);
Controls.Add(NormalizeButton);
Controls.Add(ResetButton);
Controls.Add(MatchAspectButton);
Controls.Add(EnableBoxSelect);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
Name = "SetZoomForm";
Text = "Zoom Level";
((System.ComponentModel.ISupportInitialize)ZoomTrackBar).EndInit();
Text = "Set Viewport Zoom";
ResumeLayout(false);
PerformLayout();
}
#endregion
private Label MessageLabel;
private TrackBar ZoomTrackBar;
private Label ValueLabel;
private TextBox ZoomMinValue;
private TextBox ZoomMaxValue;
private System.Windows.Forms.Button EnableBoxSelect;
private System.Windows.Forms.Button MatchAspectButton;
private System.Windows.Forms.Button ResetButton;
private System.Windows.Forms.Button NormalizeButton;
private System.Windows.Forms.TextBox MinBoxX;
private System.Windows.Forms.Label TextX;
private System.Windows.Forms.TextBox MaxBoxX;
private System.Windows.Forms.TextBox MaxBoxY;
private System.Windows.Forms.Label TextY;
private System.Windows.Forms.TextBox MinBoxY;
private System.Windows.Forms.CheckBox ViewportLock;
}
}

View File

@ -1,131 +1,227 @@
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;
private double maxZoomRange;
private readonly GraphForm refForm;
private double zoomLevel;
private bool boxSelectEnabled;
private readonly GraphForm form;
public SetZoomForm(GraphForm form)
public SetZoomForm(GraphForm refForm)
{
InitializeComponent();
this.refForm = refForm;
minZoomRange = 1 / (form.ZoomLevel * 2);
maxZoomRange = 2 / form.ZoomLevel;
zoomLevel = 1 / form.ZoomLevel;
refForm.Paint += (o, e) => RedeclareValues();
RedeclareValues();
ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum);
MinBoxX.Leave += MinBoxX_Finish;
MinBoxX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MinBoxX_Finish(o, e);
};
MaxBoxX.Leave += MaxBoxX_Finish;
MaxBoxX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MaxBoxX_Finish(o, e);
};
this.form = form;
MinBoxY.Leave += MinBoxY_Finish;
MinBoxY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MinBoxY_Finish(o, e);
};
MaxBoxY.Leave += MaxBoxY_Finish;
MaxBoxY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MaxBoxY_Finish(o, e);
};
}
protected override void OnPaint(PaintEventArgs e)
private void EnableBoxSelect_Click(object? sender, EventArgs e)
{
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
ZoomMinValue.Text = minZoomRange.ToString("0.00");
boxSelectEnabled = !boxSelectEnabled;
refForm.canBoxSelect = boxSelectEnabled;
ValueLabel.Text = $"{zoomLevel:0.00}x";
base.OnPaint(e);
form.ZoomLevel = 1 / zoomLevel;
form.Invalidate(false);
}
private double FactorToZoom(double factor)
if (boxSelectEnabled)
{
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
{
double value;
if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) ||
ZoomMinValue.Text.EndsWith('.'))
{
return;
EnableBoxSelect.Text = $"Cancel ...";
refForm.Focus();
}
else
{
value = double.Parse(ZoomMinValue.Text);
if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new();
EnableBoxSelect.Text = "Box Select";
}
}
private void MatchAspectButton_Click(object? sender, EventArgs e)
{
double zoomXFactor = refForm.ZoomLevel.x / refForm.ZoomLevel.y;
double actualXFactor = refForm.ClientRectangle.Width / refForm.ClientRectangle.Height;
double diff = actualXFactor / zoomXFactor;
int newWidth = (int)(refForm.Width / diff);
refForm.ZoomLevel = new(refForm.ZoomLevel.x * diff, refForm.ZoomLevel.y);
int maxScreenWidth = Screen.FromControl(refForm).WorkingArea.Width;
if (newWidth >= maxScreenWidth)
{
refForm.Location = new(0, refForm.Location.Y);
double xScaleFactor = (double)maxScreenWidth / newWidth;
newWidth = maxScreenWidth;
refForm.Height = (int)(refForm.Height * xScaleFactor);
refForm.ZoomLevel = new(refForm.ZoomLevel.x * xScaleFactor, refForm.ZoomLevel.y * xScaleFactor);
}
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);
refForm.Width = newWidth;
}
catch
private void NormalizeButton_Click(object? sender, EventArgs e)
{
minZoomRange = original;
ZoomMinValue.Text = minZoomRange.ToString("0.00");
double factor = 1 / Math.Min(refForm.ZoomLevel.x, refForm.ZoomLevel.y);
refForm.ZoomLevel = new(factor * refForm.ZoomLevel.x, factor * refForm.ZoomLevel.y);
}
private void ResetButton_Click(object? sender, EventArgs e)
{
refForm.ResetAllViewport();
}
private void ViewportLock_CheckedChanged(object? sender, EventArgs e)
{
refForm.ViewportLocked = ViewportLock.Checked;
RedeclareValues();
}
private void ZoomMaxValue_TextChanged(object sender, EventArgs e)
private void MinBoxX_Finish(object? sender, EventArgs e)
{
double original = maxZoomRange;
try
if (double.TryParse(MinBoxX.Text, out double minX))
{
double value;
if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) ||
ZoomMaxValue.Text.EndsWith('.'))
Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
if (minX > max.x)
{
return;
}
else
{
value = double.Parse(ZoomMaxValue.Text);
if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new();
MaxBoxX.Text = MinBoxX.Text;
MaxBoxX_Finish(sender, e);
minX = max.x;
// Redefine bounds.
min = refForm.MinVisibleGraph;
max = refForm.MaxVisibleGraph;
}
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);
double newCenterX = (minX + max.x) / 2,
zoomFactorX = (max.x - minX) / (max.x - min.x);
zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y);
refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y);
}
catch
refForm.Invalidate(false);
}
private void MaxBoxX_Finish(object? sender, EventArgs e)
{
maxZoomRange = original;
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
}
}
if (double.TryParse(MaxBoxX.Text, out double maxX))
{
Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
if (maxX < min.x)
{
MinBoxX.Text = MaxBoxX.Text;
MinBoxX_Finish(sender, e);
maxX = min.x;
// Redefine bounds.
min = refForm.MinVisibleGraph;
max = refForm.MaxVisibleGraph;
}
double newCenterX = (min.x + maxX) / 2,
zoomFactorX = (maxX - min.x) / (max.x - min.x);
refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y);
refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y);
}
refForm.Invalidate(false);
}
private void MinBoxY_Finish(object? sender, EventArgs e)
{
if (double.TryParse(MinBoxY.Text, out double minY))
{
Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
if (minY > max.y)
{
MaxBoxY.Text = MinBoxY.Text;
MaxBoxY_Finish(sender, e);
minY = max.y;
// Redefine bounds.
min = refForm.MinVisibleGraph;
max = refForm.MaxVisibleGraph;
}
double newCenterY = -(minY + max.y) / 2, // Keeping it positive flips it for some reason ???
zoomFactorY = (max.y - minY) / (max.y - min.y);
refForm.ScreenCenter = new(refForm.ScreenCenter.x, newCenterY);
refForm.ZoomLevel = new(refForm.ZoomLevel.x, refForm.ZoomLevel.y * zoomFactorY);
}
refForm.Invalidate(false);
}
private void MaxBoxY_Finish(object? sender, EventArgs e)
{
if (double.TryParse(MaxBoxY.Text, out double maxY))
{
Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
if (maxY < min.y)
{
MinBoxY.Text = MaxBoxY.Text;
MinBoxY_Finish(sender, e);
maxY = min.y;
// Redefine bounds.
min = refForm.MinVisibleGraph;
max = refForm.MaxVisibleGraph;
}
double newCenterY = -(min.y + maxY) / 2, // Keeping it positive flips it for some reason ???
zoomFactorY = (maxY - min.y) / (max.y - min.y);
refForm.ScreenCenter = new(refForm.ScreenCenter.x, newCenterY);
refForm.ZoomLevel = new(refForm.ZoomLevel.x, refForm.ZoomLevel.y * zoomFactorY);
}
refForm.Invalidate(false);
}
public void RedeclareValues()
{
bool enabled = !refForm.ViewportLocked;
Float2 minGraph = refForm.MinVisibleGraph,
maxGraph = refForm.MaxVisibleGraph;
MinBoxX.Text = $"{minGraph.x:0.000}";
MaxBoxX.Text = $"{maxGraph.x:0.000}";
MinBoxY.Text = $"{minGraph.y:0.000}";
MaxBoxY.Text = $"{maxGraph.y:0.000}";
ViewportLock.Checked = !enabled;
EnableBoxSelect.Enabled = enabled;
MatchAspectButton.Enabled = enabled;
NormalizeButton.Enabled = enabled;
ResetButton.Enabled = enabled;
MinBoxX.Enabled = enabled;
MaxBoxX.Enabled = enabled;
MinBoxY.Enabled = enabled;
MaxBoxY.Enabled = enabled;
}
internal void CompleteBoxSelection()
{
if (boxSelectEnabled) EnableBoxSelect_Click(null, new());
}
}

View File

@ -0,0 +1,147 @@
namespace Graphing.Forms
{
partial class SlopeFieldDetailForm
{
/// <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()
{
Message = new System.Windows.Forms.Label();
TrackSlopeDetail = new System.Windows.Forms.TrackBar();
MinDetailBox = new System.Windows.Forms.TextBox();
MaxDetailBox = new System.Windows.Forms.TextBox();
CurrentDetailBox = new System.Windows.Forms.TextBox();
IncrementButton = new System.Windows.Forms.Button();
DecrementButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)TrackSlopeDetail).BeginInit();
SuspendLayout();
//
// Message
//
Message.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
Message.Location = new System.Drawing.Point(119, 25);
Message.Margin = new System.Windows.Forms.Padding(110);
Message.Name = "Message";
Message.Size = new System.Drawing.Size(516, 109);
Message.TabIndex = 1;
Message.Text = "Change the Detail of %name%\r\nA higher value means more lines per unit.";
Message.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// TrackSlopeDetail
//
TrackSlopeDetail.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
TrackSlopeDetail.LargeChange = 250;
TrackSlopeDetail.Location = new System.Drawing.Point(59, 158);
TrackSlopeDetail.Margin = new System.Windows.Forms.Padding(50);
TrackSlopeDetail.Maximum = 1000;
TrackSlopeDetail.Name = "TrackSlopeDetail";
TrackSlopeDetail.Size = new System.Drawing.Size(636, 90);
TrackSlopeDetail.SmallChange = 0;
TrackSlopeDetail.TabIndex = 0;
TrackSlopeDetail.TickFrequency = 0;
TrackSlopeDetail.TickStyle = System.Windows.Forms.TickStyle.Both;
TrackSlopeDetail.Scroll += TrackSlopeDetail_Scroll;
//
// MinDetailBox
//
MinDetailBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
MinDetailBox.Location = new System.Drawing.Point(12, 228);
MinDetailBox.Name = "MinDetailBox";
MinDetailBox.Size = new System.Drawing.Size(100, 39);
MinDetailBox.TabIndex = 2;
//
// MaxDetailBox
//
MaxDetailBox.Anchor = System.Windows.Forms.AnchorStyles.Right;
MaxDetailBox.Location = new System.Drawing.Point(642, 228);
MaxDetailBox.Name = "MaxDetailBox";
MaxDetailBox.Size = new System.Drawing.Size(100, 39);
MaxDetailBox.TabIndex = 3;
MaxDetailBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
//
// CurrentDetailBox
//
CurrentDetailBox.Anchor = System.Windows.Forms.AnchorStyles.None;
CurrentDetailBox.Location = new System.Drawing.Point(330, 228);
CurrentDetailBox.Name = "CurrentDetailBox";
CurrentDetailBox.Size = new System.Drawing.Size(100, 39);
CurrentDetailBox.TabIndex = 4;
CurrentDetailBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// IncrementButton
//
IncrementButton.Anchor = System.Windows.Forms.AnchorStyles.None;
IncrementButton.Font = new System.Drawing.Font("Segoe UI", 7.875F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
IncrementButton.Location = new System.Drawing.Point(436, 228);
IncrementButton.Name = "IncrementButton";
IncrementButton.Size = new System.Drawing.Size(40, 40);
IncrementButton.TabIndex = 5;
IncrementButton.Text = "+";
IncrementButton.UseVisualStyleBackColor = true;
IncrementButton.Click += IncrementButton_Click;
//
// DecrementButton
//
DecrementButton.Anchor = System.Windows.Forms.AnchorStyles.None;
DecrementButton.Font = new System.Drawing.Font("Segoe UI", 7.875F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
DecrementButton.Location = new System.Drawing.Point(284, 228);
DecrementButton.Name = "DecrementButton";
DecrementButton.Size = new System.Drawing.Size(40, 40);
DecrementButton.TabIndex = 6;
DecrementButton.Text = "-";
DecrementButton.UseVisualStyleBackColor = true;
DecrementButton.Click += DecrementButton_Click;
//
// SlopeFieldDetailForm
//
AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(754, 282);
Controls.Add(DecrementButton);
Controls.Add(IncrementButton);
Controls.Add(CurrentDetailBox);
Controls.Add(MaxDetailBox);
Controls.Add(MinDetailBox);
Controls.Add(Message);
Controls.Add(TrackSlopeDetail);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
Name = "SlopeFieldDetailForm";
Text = "Change Slope Field Detail";
((System.ComponentModel.ISupportInitialize)TrackSlopeDetail).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label Message;
private System.Windows.Forms.TrackBar TrackSlopeDetail;
private System.Windows.Forms.TextBox MinDetailBox;
private System.Windows.Forms.TextBox MaxDetailBox;
private System.Windows.Forms.TextBox CurrentDetailBox;
private System.Windows.Forms.Button IncrementButton;
private System.Windows.Forms.Button DecrementButton;
}
}

View File

@ -0,0 +1,130 @@
using Graphing.Graphables;
using System;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class SlopeFieldDetailForm : Form
{
private readonly GraphForm refForm;
private readonly SlopeField slopeField;
private double minDetail, maxDetail;
public SlopeFieldDetailForm(GraphForm form, SlopeField sf)
{
InitializeComponent();
refForm = form;
slopeField = sf;
refForm.Paint += (o, e) => RedeclareValues();
RedeclareValues();
TrackSlopeDetail.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Right) IncrementButton_Click(o, e);
else if (e.KeyCode == Keys.Left) DecrementButton_Click(o, e);
};
MinDetailBox.Leave += MinDetailBox_Finish;
MinDetailBox.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MinDetailBox_Finish(o, e);
};
MaxDetailBox.Leave += MaxDetailBox_Finish;
MaxDetailBox.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MaxDetailBox_Finish(o, e);
};
CurrentDetailBox.Leave += CurrentDetailBox_Finish;
CurrentDetailBox.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) CurrentDetailBox_Finish(o, e);
};
minDetail = sf.Detail / 2;
maxDetail = sf.Detail * 2;
Message.Text = Message.Text.Replace("%name%", sf.Name);
}
// Exponential interpolations are better than simple lerps here since
// we're scaling a multiple rather than an additive.
private double Interp(double t)
{
// This is weird. I don't like the +1s and -1s, I don't think I wrote this right.
// But it seems to get the job done.
return minDetail + Math.Pow(2, t * Math.Log2(maxDetail - minDetail + 1)) - 1;
}
private double InverseInterp(double c)
{
return Math.Log2(c - minDetail + 1) / Math.Log2(maxDetail - minDetail + 1);
}
private void RedeclareValues()
{
double detail = slopeField.Detail;
if (detail < minDetail) minDetail = detail;
else if (detail > maxDetail) maxDetail = detail;
double t = InverseInterp(detail);
TrackSlopeDetail.Value = (int)(TrackSlopeDetail.Minimum + t * (TrackSlopeDetail.Maximum - TrackSlopeDetail.Minimum));
MinDetailBox.Text = $"{minDetail:0.00}";
MaxDetailBox.Text = $"{maxDetail:0.00}";
CurrentDetailBox.Text = $"{detail:0.00}";
}
private void TrackSlopeDetail_Scroll(object? sender, EventArgs e)
{
double t = (double)(TrackSlopeDetail.Value - TrackSlopeDetail.Minimum) / (TrackSlopeDetail.Maximum - TrackSlopeDetail.Minimum);
double newDetail = Interp(t);
slopeField.Detail = newDetail;
refForm.Invalidate(false);
}
private void MinDetailBox_Finish(object? sender, EventArgs e)
{
if (double.TryParse(MinDetailBox.Text, out double newMinDetail))
{
minDetail = newMinDetail;
if (minDetail > slopeField.Detail) slopeField.Detail = newMinDetail;
}
refForm.Invalidate(false);
}
private void MaxDetailBox_Finish(object? sender, EventArgs e)
{
if (double.TryParse(MaxDetailBox.Text, out double newMaxDetail))
{
maxDetail = newMaxDetail;
if (maxDetail < slopeField.Detail) slopeField.Detail = newMaxDetail;
}
refForm.Invalidate(false);
}
private void CurrentDetailBox_Finish(object? sender, EventArgs e)
{
if (double.TryParse(CurrentDetailBox.Text, out double newDetail))
{
if (newDetail < minDetail) minDetail = newDetail;
else if (newDetail > maxDetail) maxDetail = newDetail;
slopeField.Detail = newDetail;
}
refForm.Invalidate(false);
}
private void IncrementButton_Click(object? sender, EventArgs e)
{
double newDetail = slopeField.Detail * 1.0625f;
if (newDetail > maxDetail) maxDetail = newDetail;
slopeField.Detail = newDetail;
refForm.Invalidate(false);
}
private void DecrementButton_Click(object? sender, EventArgs e)
{
double newDetail = slopeField.Detail / 1.0625f;
if (newDetail < minDetail) minDetail = newDetail;
slopeField.Detail = newDetail;
refForm.Invalidate(false);
}
}

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>

204
Base/Forms/TranslateForm.Designer.cs generated Normal file
View File

@ -0,0 +1,204 @@
namespace Graphing.Forms
{
partial class TranslateForm
{
/// <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()
{
TrackX = new System.Windows.Forms.TrackBar();
LabelX = new System.Windows.Forms.Label();
MinBoxX = new System.Windows.Forms.TextBox();
MaxBoxX = new System.Windows.Forms.TextBox();
ThisValueX = new System.Windows.Forms.TextBox();
ThisValueY = new System.Windows.Forms.TextBox();
MaxBoxY = new System.Windows.Forms.TextBox();
MinBoxY = new System.Windows.Forms.TextBox();
LabelY = new System.Windows.Forms.Label();
TrackY = new System.Windows.Forms.TrackBar();
TitleLabel = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)TrackX).BeginInit();
((System.ComponentModel.ISupportInitialize)TrackY).BeginInit();
SuspendLayout();
//
// TrackX
//
TrackX.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
TrackX.LargeChange = 250;
TrackX.Location = new System.Drawing.Point(15, 193);
TrackX.Margin = new System.Windows.Forms.Padding(0);
TrackX.Maximum = 1000;
TrackX.Name = "TrackX";
TrackX.Size = new System.Drawing.Size(644, 90);
TrackX.SmallChange = 50;
TrackX.TabIndex = 0;
TrackX.TabStop = false;
TrackX.TickFrequency = 50;
TrackX.TickStyle = System.Windows.Forms.TickStyle.Both;
TrackX.Value = 1;
TrackX.Scroll += TrackX_Scroll;
//
// LabelX
//
LabelX.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
LabelX.Location = new System.Drawing.Point(15, 157);
LabelX.Name = "LabelX";
LabelX.Size = new System.Drawing.Size(644, 36);
LabelX.TabIndex = 1;
LabelX.Text = "X Offset";
LabelX.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// MinBoxX
//
MinBoxX.Location = new System.Drawing.Point(15, 259);
MinBoxX.Name = "MinBoxX";
MinBoxX.Size = new System.Drawing.Size(100, 39);
MinBoxX.TabIndex = 2;
//
// MaxBoxX
//
MaxBoxX.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
MaxBoxX.Location = new System.Drawing.Point(556, 259);
MaxBoxX.Name = "MaxBoxX";
MaxBoxX.Size = new System.Drawing.Size(100, 39);
MaxBoxX.TabIndex = 3;
MaxBoxX.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
//
// ThisValueX
//
ThisValueX.Anchor = System.Windows.Forms.AnchorStyles.Top;
ThisValueX.Location = new System.Drawing.Point(289, 259);
ThisValueX.Name = "ThisValueX";
ThisValueX.Size = new System.Drawing.Size(100, 39);
ThisValueX.TabIndex = 4;
ThisValueX.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// ThisValueY
//
ThisValueY.Anchor = System.Windows.Forms.AnchorStyles.Top;
ThisValueY.Location = new System.Drawing.Point(289, 449);
ThisValueY.Name = "ThisValueY";
ThisValueY.Size = new System.Drawing.Size(100, 39);
ThisValueY.TabIndex = 9;
ThisValueY.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// MaxBoxY
//
MaxBoxY.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
MaxBoxY.Location = new System.Drawing.Point(556, 449);
MaxBoxY.Name = "MaxBoxY";
MaxBoxY.Size = new System.Drawing.Size(100, 39);
MaxBoxY.TabIndex = 8;
MaxBoxY.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
//
// MinBoxY
//
MinBoxY.Location = new System.Drawing.Point(15, 449);
MinBoxY.Name = "MinBoxY";
MinBoxY.Size = new System.Drawing.Size(100, 39);
MinBoxY.TabIndex = 7;
//
// LabelY
//
LabelY.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
LabelY.Location = new System.Drawing.Point(15, 347);
LabelY.Name = "LabelY";
LabelY.Size = new System.Drawing.Size(644, 36);
LabelY.TabIndex = 6;
LabelY.Text = "Y Offset";
LabelY.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// TrackY
//
TrackY.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
TrackY.LargeChange = 250;
TrackY.Location = new System.Drawing.Point(15, 383);
TrackY.Margin = new System.Windows.Forms.Padding(0);
TrackY.Maximum = 1000;
TrackY.Name = "TrackY";
TrackY.Size = new System.Drawing.Size(644, 90);
TrackY.SmallChange = 50;
TrackY.TabIndex = 5;
TrackY.TabStop = false;
TrackY.TickFrequency = 50;
TrackY.TickStyle = System.Windows.Forms.TickStyle.Both;
TrackY.Value = 1;
TrackY.Scroll += TrackY_Scroll;
//
// TitleLabel
//
TitleLabel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
TitleLabel.Location = new System.Drawing.Point(12, 39);
TitleLabel.Name = "TitleLabel";
TitleLabel.Padding = new System.Windows.Forms.Padding(0, 0, 0, 18);
TitleLabel.Size = new System.Drawing.Size(644, 89);
TitleLabel.TabIndex = 10;
TitleLabel.Text = "Change the Location of %name%";
TitleLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// TranslateForm
//
AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
AutoSize = true;
AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
ClientSize = new System.Drawing.Size(674, 531);
Controls.Add(TitleLabel);
Controls.Add(ThisValueY);
Controls.Add(MaxBoxY);
Controls.Add(MinBoxY);
Controls.Add(LabelY);
Controls.Add(TrackY);
Controls.Add(ThisValueX);
Controls.Add(MaxBoxX);
Controls.Add(MinBoxX);
Controls.Add(LabelX);
Controls.Add(TrackX);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
Name = "TranslateForm";
Padding = new System.Windows.Forms.Padding(15);
Text = "Herm";
TopMost = true;
((System.ComponentModel.ISupportInitialize)TrackX).EndInit();
((System.ComponentModel.ISupportInitialize)TrackY).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.TrackBar TrackX;
private System.Windows.Forms.Label LabelX;
private System.Windows.Forms.TextBox MinBoxX;
private System.Windows.Forms.TextBox MaxBoxX;
private System.Windows.Forms.TextBox ThisValueX;
private System.Windows.Forms.TextBox ThisValueY;
private System.Windows.Forms.TextBox MaxBoxY;
private System.Windows.Forms.TextBox MinBoxY;
private System.Windows.Forms.Label LabelY;
private System.Windows.Forms.TrackBar TrackY;
private System.Windows.Forms.Label TitleLabel;
}
}

285
Base/Forms/TranslateForm.cs Normal file
View File

@ -0,0 +1,285 @@
using Graphing.Abstract;
using System;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class TranslateForm : Form
{
private readonly GraphForm refForm;
// These variables both represent the same graphable.
private readonly ITranslatableX? ableTransX;
private readonly ITranslatableY? ableTransY;
private readonly bool useX;
private readonly bool useY;
private double minX, maxX, curX, minY, maxY, curY;
public TranslateForm(GraphForm graph, Graphable ableRaw, ITranslatable ableTrans)
{
InitializeComponent();
Text = $"Translate {ableRaw.Name}";
TitleLabel.Text = $"Adjust Location for {ableRaw.Name}";
MinBoxX.Leave += (o, e) => UpdateFromMinBoxX();
MinBoxX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) UpdateFromMinBoxX();
};
MaxBoxX.Leave += (o, e) => UpdateFromMaxBoxX();
MaxBoxX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) UpdateFromMaxBoxX();
};
ThisValueX.Leave += (o, e) => UpdateFromThisBoxX();
ThisValueX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) UpdateFromThisBoxX();
};
MinBoxY.Leave += (o, e) => UpdateFromMinBoxY();
MinBoxY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) UpdateFromMinBoxY();
};
MaxBoxY.Leave += (o, e) => UpdateFromMaxBoxY();
MaxBoxY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) UpdateFromMaxBoxY();
};
ThisValueY.Leave += (o, e) => UpdateFromThisBoxY();
ThisValueY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) UpdateFromThisBoxY();
};
refForm = graph;
double curX = 0, curY = 0;
if (ableTrans is ITranslatableX transX)
{
useX = true;
ableTransX = transX;
curX = transX.OffsetX;
}
else
{
LabelY.Location = LabelX.Location;
TrackY.Location = TrackX.Location;
MinBoxY.Location = MinBoxX.Location;
MaxBoxY.Location = MaxBoxX.Location;
ThisValueY.Location = ThisValueX.Location;
LabelX.Dispose();
TrackX.Dispose();
MinBoxX.Dispose();
MaxBoxX.Dispose();
ThisValueX.Dispose();
}
if (ableTrans is ITranslatableY transY)
{
useY = true;
ableTransY = transY;
curY = transY.OffsetY;
}
else
{
LabelY.Dispose();
TrackY.Dispose();
MinBoxY.Dispose();
MaxBoxY.Dispose();
ThisValueY.Dispose();
}
if (!useX && !useY)
{
TitleLabel.Text = $"There doesn't seem to be anything you can translate for {ableRaw.Name}.";
}
// TODO: Maybe replace these default limits with what's visible on screen?
// Tried it and it got a bit confusing so maybe not.
minX = -10;
maxX = 10;
minY = -10;
maxY = 10;
UpdateFromCurX(curX, false);
UpdateFromCurY(curY, false);
}
private void UpdateFromCurX(double newCurX, bool invalidate)
{
curX = newCurX;
if (curX < minX) minX = curX;
else if (curX > maxX) maxX = curX;
int step = (int)(1000 * InverseLerp(minX, maxX, curX));
TrackX.Value = step;
MinBoxX.Text = $"{minX:0.00}";
MaxBoxX.Text = $"{maxX:0.00}";
ThisValueX.Text = $"{curX:0.00}";
if (invalidate) refForm.Invalidate(false);
}
private void UpdateFromSliderX(bool invalidate)
{
double t = InverseLerp(0, 1000, TrackX.Value);
curX = Lerp(minX, maxX, t);
ThisValueX.Text = $"{curX:0.00}";
ableTransX!.OffsetX = curX;
if (invalidate) refForm.Invalidate(false);
}
private void UpdateFromMinBoxX()
{
if (!double.TryParse(MinBoxX.Text, out double newMin))
{
MinBoxX.Text = $"{minX:0.00}";
return;
}
minX = newMin;
MinBoxX.Text = $"{minX:0.00}";
if (minX > curX)
{
curX = minX;
ThisValueX.Text = $"{curX:0.00}";
ableTransX!.OffsetX = curX;
}
int step = (int)(1000 * InverseLerp(minX, maxX, curX));
TrackX.Value = step;
refForm.Invalidate(false);
}
private void UpdateFromMaxBoxX()
{
if (!double.TryParse(MaxBoxX.Text, out double newMax))
{
MaxBoxX.Text = $"{maxX:0.00}";
return;
}
maxX = newMax;
MaxBoxX.Text = $"{maxX:0.00}";
if (maxX < curX)
{
curX = maxX;
ThisValueX.Text = $"{curX:0.00}";
ableTransX!.OffsetX = curX;
}
int step = (int)(1000 * InverseLerp(minX, maxX, curX));
TrackX.Value = step;
refForm.Invalidate(false);
}
private void UpdateFromThisBoxX()
{
if (!double.TryParse(ThisValueX.Text, out double newCur))
{
ThisValueX.Text = $"{curX:0.00}";
return;
}
ableTransX!.OffsetX = newCur;
UpdateFromCurX(newCur, true);
}
private void UpdateFromCurY(double newCurY, bool invalidate)
{
curY = newCurY;
if (curY < minY) minY = curY;
else if (curY > maxY) maxY = curY;
int step = (int)(1000 * InverseLerp(minY, maxY, curY));
TrackY.Value = step;
MinBoxY.Text = $"{minY:0.00}";
MaxBoxY.Text = $"{maxY:0.00}";
ThisValueY.Text = $"{curY:0.00}";
if (invalidate) refForm.Invalidate(false);
}
private void UpdateFromSliderY(bool invalidate)
{
double t = InverseLerp(0, 1000, TrackY.Value);
curY = Lerp(minY, maxY, t);
ThisValueY.Text = $"{curY:0.00}";
ableTransY!.OffsetY = curY;
if (invalidate) refForm.Invalidate(false);
}
private void UpdateFromMinBoxY()
{
if (!double.TryParse(MinBoxY.Text, out double newMin))
{
MinBoxY.Text = $"{minY:0.00}";
return;
}
minY = newMin;
MinBoxY.Text = $"{minY:0.00}";
if (minY > curY)
{
curY = minY;
ThisValueY.Text = $"{curY:0.00}";
ableTransY!.OffsetY = curY;
}
int step = (int)(1000 * InverseLerp(minY, maxY, curY));
TrackY.Value = step;
refForm.Invalidate(false);
}
private void UpdateFromMaxBoxY()
{
if (!double.TryParse(MaxBoxY.Text, out double newMax))
{
MaxBoxY.Text = $"{maxY:0.00}";
return;
}
maxY = newMax;
MaxBoxY.Text = $"{maxY:0.00}";
if (maxY < curY)
{
curY = maxY;
ThisValueY.Text = $"{curY:0.00}";
ableTransY!.OffsetY = curY;
}
int step = (int)(1000 * InverseLerp(minY, maxY, curY));
TrackY.Value = step;
refForm.Invalidate(false);
}
private void UpdateFromThisBoxY()
{
if (!double.TryParse(ThisValueY.Text, out double newCur))
{
ThisValueY.Text = $"{curY:0.00}";
return;
}
ableTransY!.OffsetY = newCur;
UpdateFromCurY(newCur, true);
}
private static double Lerp(double a, double b, double t) => a + t * (b - a);
private static double InverseLerp(double a, double b, double c) => (c - a) / (b - a);
private void TrackX_Scroll(object sender, EventArgs e)
{
UpdateFromSliderX(true);
}
private void TrackY_Scroll(object sender, EventArgs e)
{
UpdateFromSliderY(true);
}
}

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>

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

@ -0,0 +1,100 @@
using System.Drawing;
using System.Windows.Forms;
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,98 @@
using Graphing.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
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();
if (thisBytes == 0) continue;
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];
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 * buttonHeight),
Parent = SpecificCachePanel,
Size = new Size(SpecificCachePanel.Width - buttonSpaced, buttonHeight),
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 - buttonWidth, buttonCache.Count * buttonHeight),
Parent = SpecificCachePanel,
Size = new Size(buttonWidth, buttonHeight),
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,6 @@
using Graphing.Forms;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing;
@ -7,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; }
@ -26,5 +28,14 @@ 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 Graphable ShallowCopy();
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 IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) => [];
}

View File

@ -0,0 +1,133 @@
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
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)
{
Color = equation.Color;
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 long GetCacheBytes() => 16 * tableXY.Count;
public override Graphable ShallowCopy() => new ColumnTable(width / 0.75, tableXY);
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), 0.625));
}
return items;
}
public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
{
// Get closest value to mouse pos.
double closestDist = double.PositiveInfinity, closestX = 0, closestY = 0;
foreach (KeyValuePair<double, double> points in tableXY)
{
double dist = Math.Abs(points.Key - graphMousePos.x);
if (dist < closestDist)
{
closestDist = dist;
closestX = points.Key;
closestY = points.Value;
}
}
Int2 screenMousePos = graph.GraphSpaceToScreenSpace(graphMousePos);
Int2 minBox = graph.GraphSpaceToScreenSpace(new(closestX - width / 2, 0)),
maxBox = graph.GraphSpaceToScreenSpace(new(closestX + width / 2, closestY));
int distX, distY;
if (screenMousePos.x < minBox.x) distX = minBox.x - screenMousePos.x; // On left side.
else if (screenMousePos.x > maxBox.x) distX = screenMousePos.x - maxBox.x; // On right side.
else distX = 0; // Inside.
if (closestY > 0)
{
if (screenMousePos.y > minBox.y) distY = screenMousePos.y - minBox.y; // Underneath.
else if (screenMousePos.y < maxBox.y) distY = maxBox.y - screenMousePos.y; // Above.
else distY = 0; // Inside.
}
else
{
if (screenMousePos.y < minBox.y) distY = minBox.y - screenMousePos.y; // Underneath.
else if (screenMousePos.y > maxBox.y) distY = screenMousePos.y - maxBox.y; // Above.
else distY = 0; // Inside.
}
int totalDist = (int)Math.Sqrt(distX * distX + distY * distY);
return totalDist < 50 * factor * graph.DpiFloat / 192;
}
public override IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
{
// Get closest value to mouse pos.
double closestDist = double.PositiveInfinity, closestX = 0, closestY = 0;
foreach (KeyValuePair<double, double> points in tableXY)
{
double dist = Math.Abs(points.Key - graphMousePos.x);
if (dist < closestDist)
{
closestDist = dist;
closestX = points.Key;
closestY = points.Value;
}
}
Float2 textPoint = new(closestX, closestY);
Int2 offset;
ContentAlignment alignment;
if (textPoint.y >= 0)
{
offset = new(0, -5);
alignment = ContentAlignment.BottomCenter;
}
else
{
offset = new(0, 5);
alignment = ContentAlignment.TopCenter;
}
return
[
new GraphUiText($"{closestY:0.00}", textPoint, alignment, offsetPix: offset)
];
}
// Nothing to preload, everything is already cached.
public override void Preload(Float2 xRange, Float2 yRange, double step) { }
}

View File

@ -1,12 +1,27 @@
using Graphing.Forms;
using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables;
public class Equation : Graphable
public class Equation : Graphable, IIntegrable, IDerivable, ITranslatableXY, IConvertSlopeField,
IConvertColumnTable
{
private static int equationNum;
private readonly EquationDelegate equ;
public bool UngraphWhenConvertedToColumnTable => false;
public bool UngraphWhenConvertedToSlopeField => false;
public double OffsetX { get; set; }
public double OffsetY { get; set; }
protected readonly EquationDelegate equ;
protected readonly List<Float2> cache;
public event Action<GraphForm> OnInvalidate;
public Equation(EquationDelegate equ)
{
@ -14,28 +29,140 @@ public class Equation : Graphable
Name = $"Equation {equationNum}";
this.equ = equ;
cache = [];
OffsetX = 0;
OffsetY = 0;
OnInvalidate = delegate { };
}
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;
epsilon *= graph.DpiFloat / 192;
List<IGraphPart> lines = [];
double previousX = graph.MinVisibleGraph.x;
double previousY = equ(previousX);
for (int i = 1; i < graph.ClientRectangle.Width; i += 10)
double previousY = GetFromCache(previousX, epsilon);
for (int i = 0; i < graph.ClientRectangle.Width + step; i += step)
{
double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x;
double currentY = equ(currentX);
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;
}
OnInvalidate.Invoke(graph);
return lines;
}
protected double DerivativeAtPoint(double x)
{
const double step = 1e-3;
return (equ(x + step - OffsetX) - equ(x - OffsetX)) / step;
}
public Graphable Derive() => new Equation(DerivativeAtPoint);
public Graphable Integrate() => new IntegralEquation(this);
public EquationDelegate GetDelegate() => equ;
public SlopeField ToSlopeField(int detail) => new(detail, (x, y) => DerivativeAtPoint(x))
{
Color = Color,
Name = $"Slope Field of {Name}"
};
public ColumnTable ToColumnTable(double start, double end, int detail)
=> new(1.0 / detail, this, start, end);
public override void EraseCache() => cache.Clear();
protected double GetFromCache(double x, double epsilon)
{
(double dist, double nearest, int index) = NearestCachedPoint(x - OffsetX);
if (dist < epsilon) return nearest + OffsetY;
else
{
double result = equ(x - OffsetX);
cache.Insert(index + 1, new(x - OffsetX, result));
return result + OffsetY;
}
}
public double GetValueAt(double x) => GetFromCache(x, 0);
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 ShallowCopy() => 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 IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
{
Float2 point = new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3));
return
[
new GraphUiText($"({point.x:0.00}, {point.y:0.00})", point, ContentAlignment.BottomLeft),
new GraphUiCircle(point),
];
}
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);

View File

@ -0,0 +1,96 @@
using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables;
public class EquationDifference : Graphable, ITranslatableX, IConvertEquation
{
public bool UngraphWhenConvertedToEquation => true;
public double Position
{
get => _position;
set
{
_position = value;
points = new Float2(equA.GetValueAt(value), equB.GetValueAt(value));
}
}
private double _position;
public double OffsetX
{
get => Position;
set => Position = value;
}
protected readonly Equation equA, equB;
protected Float2 points; // X represents equA.y, Y represents equB.y
public EquationDifference(double position, Equation equA, Equation equB)
{
this.equA = equA;
this.equB = equB;
Name = $"Difference between {equA.Name} and {equB.Name}";
Position = position;
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
Float2 pA = new(Position, points.x),
pB = new(Position, points.y);
return
[
new GraphUiCircle(pA),
new GraphUiCircle(pB),
new GraphLine(pA, pB)
];
}
public double DistanceAtPoint(double x) => equA.GetValueAt(x) - equB.GetValueAt(x);
public override Graphable ShallowCopy() => new EquationDifference(Position, equA, equB);
public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
{
Float2 nearestPoint = new(Position, graphMousePos.y);
double upper = double.Max(points.x, points.y),
lower = double.Min(points.x, points.y);
if (nearestPoint.y > upper) nearestPoint.y = upper;
else if (nearestPoint.y < lower) nearestPoint.y = lower;
Int2 nearestPixelPoint = graph.GraphSpaceToScreenSpace(nearestPoint);
Int2 screenMousePos = graph.GraphSpaceToScreenSpace(graphMousePos);
Int2 diff = new(screenMousePos.x - nearestPixelPoint.x,
screenMousePos.y - nearestPixelPoint.y);
int dist = (int)Math.Sqrt(diff.x * diff.x + diff.y * diff.y);
return dist < 50 * factor * graph.DpiFloat / 192;
}
public override IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
{
Float2 nearestPoint = new(Position, graphMousePos.y);
double upper = double.Max(points.x, points.y),
lower = double.Min(points.x, points.y);
if (nearestPoint.y > upper) nearestPoint.y = upper;
else if (nearestPoint.y < lower) nearestPoint.y = lower;
return
[
new GraphUiText($"Δ = {points.x - points.y:0.000}", nearestPoint, ContentAlignment.MiddleLeft, offsetPix: new Int2(15, 0)),
new GraphUiCircle(nearestPoint)
];
}
public Equation ToEquation() => new(DistanceAtPoint)
{
Color = Color,
Name = Name
};
}

View File

@ -0,0 +1,239 @@
using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
namespace Graphing.Graphables;
public class IntegralEquation : Graphable, IIntegrable, IDerivable
{
protected readonly Equation? baseEqu;
protected readonly EquationDelegate? baseEquDel;
protected readonly IntegralEquation? altBaseEqu;
protected readonly bool usingAlt;
public IntegralEquation(Equation baseEquation)
{
string oldName = baseEquation.Name, newName;
if (oldName.StartsWith("Integral of ")) newName = "Second Integral of " + oldName[12..];
else if (oldName.StartsWith("Second Integral of ")) newName = "Third Integral of " + oldName[19..];
else newName = "Integral of " + oldName;
Name = newName;
baseEqu = baseEquation;
baseEquDel = baseEquation.GetDelegate();
altBaseEqu = null;
usingAlt = false;
}
public IntegralEquation(IntegralEquation baseEquation)
{
string oldName = baseEquation.Name, newName;
if (oldName.StartsWith("Integral of ")) newName = "Second Integral of " + oldName[12..];
else if (oldName.StartsWith("Second Integral of ")) newName = "Third Integral of " + oldName[19..];
else newName = "Integral of " + oldName;
Name = newName;
baseEqu = null;
baseEquDel = null;
altBaseEqu = baseEquation;
usingAlt = true;
}
public override Graphable ShallowCopy() => 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 - baseEqu!.OffsetX) + baseEqu.OffsetY) * 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!.ShallowCopy();
else return (Equation)baseEqu!.ShallowCopy();
}
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 IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) =>
[new GraphUiCircle(new(graphMousePos.x, IntegralAtPoint(graphMousePos.x)))];
}

View File

@ -0,0 +1,134 @@
using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
namespace Graphing.Graphables;
public class ParametricEquation : Graphable, IDerivable, ITranslatableXY
{
private static int equationNum;
public double OffsetX { get; set; }
public double OffsetY { get; set; }
public double InitialT { get; set; }
public double FinalT { get; set; }
protected readonly ParametricDelegate equX, equY;
protected readonly List<(double t, Float2 point)> cache;
public ParametricEquation(double initialT, double finalT,
ParametricDelegate equX, ParametricDelegate equY)
{
equationNum++;
Name = $"Parametric Equation {equationNum}";
InitialT = initialT;
FinalT = finalT;
this.equX = equX;
this.equY = equY;
cache = [];
}
public override Graphable ShallowCopy() => new ParametricEquation(InitialT, FinalT, equX, equY);
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, 0)).x);
List<IGraphPart> lines = [];
Float2 previousPoint = GetFromCache(InitialT, epsilon);
for (double t = InitialT; t <= FinalT; t += epsilon)
{
Float2 currentPoint = GetFromCache(t, epsilon);
if (graph.IsGraphPointVisible(currentPoint) ||
graph.IsGraphPointVisible(previousPoint))
lines.Add(new GraphLine(previousPoint, currentPoint));
previousPoint = currentPoint;
}
return lines;
}
public Graphable Derive() =>
new ParametricEquation(InitialT, FinalT, GetDerivativeAtPointX, GetDerivativeAtPointY);
public ParametricDelegate GetXDelegate() => equX;
public ParametricDelegate GetYDelegate() => equY;
public double GetDerivativeAtPointX(double t)
{
const double step = 1e-3;
return (equX(t + step) - equX(t)) / step;
}
public double GetDerivativeAtPointY(double t)
{
const double step = 1e-3;
return (equY(t + step) - equY(t)) / step;
}
public Float2 GetDerivativeAtPoint(double t) =>
new(GetDerivativeAtPointX(t), GetDerivativeAtPointY(t));
public Float2 GetPointAt(double t) => GetFromCache(t, 0);
public override void EraseCache() => cache.Clear();
protected Float2 GetFromCache(double t, double epsilon)
{
(double dist, Float2 nearest, int index) = NearestCachedPoint(t);
if (dist < epsilon) return new(nearest.x + OffsetX, nearest.y + OffsetY);
else
{
Float2 result = new(equX(t), equY(t));
cache.Insert(index + 1, (t, result));
return new(result.x + OffsetX, result.y + OffsetY);
}
}
public override long GetCacheBytes() => cache.Count * 24;
protected (double dist, Float2 point, int index) NearestCachedPoint(double t)
{
if (cache.Count <= 1) return (double.PositiveInfinity, new(double.NaN, double.NaN), -1);
else if (cache.Count == 1)
{
(double resultT, Float2 resultPoint) = cache[0];
return (Math.Abs(resultT - t), resultPoint, 0);
}
else
{
int boundA = 0, boundB = cache.Count;
do
{
int boundC = (boundA + boundB) / 2;
(double thisT, Float2 thisPoint) = cache[boundC];
if (thisT == t) return (0, thisPoint, boundC);
else if (thisT > t)
{
boundA = boundC;
}
else // thisT < t
{
boundB = boundC;
}
} while (boundB - boundA > 1);
(double resultT, Float2 resultPoint) = cache[boundA];
return (Math.Abs(resultT - t), resultPoint, boundA);
}
}
public override void Preload(Float2 xRange, Float2 yRange, double step)
{
for (double t = InitialT; t <= FinalT; t += step) GetFromCache(t, step);
}
}
public delegate double ParametricDelegate(double t);

View File

@ -1,4 +1,8 @@
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables;
@ -6,37 +10,61 @@ public class SlopeField : Graphable
{
private static int slopeFieldNum;
private readonly SlopeFieldsDelegate equ;
private readonly double detail;
public double Detail
{
get => _detail;
set
{
if (Math.Abs(value - Detail) >= 1e-4)
{
// When changing detail, we need to regenerate all
// the lines. Inefficient, I know. Might be optimized
// in a future update.
EraseCache();
}
_detail = value;
}
}
private double _detail;
public SlopeField(int detail, SlopeFieldsDelegate equ)
protected readonly SlopeFieldsDelegate equ;
protected readonly List<(Float2, GraphLine)> cache;
public SlopeField(double detail, SlopeFieldsDelegate equ)
{
slopeFieldNum++;
Name = $"Slope Field {slopeFieldNum}";
this.equ = equ;
this.detail = detail;
_detail = detail;
cache = [];
}
public override IEnumerable<Line2d> GetItemsToRender(in GraphForm graph)
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
List<Line2d> lines = [];
double step = 1 / _detail;
double epsilon = step * 0.5;
List<IGraphPart> lines = [];
for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1 / detail)
double minX = Math.Round((graph.MinVisibleGraph.x - 1) / step) * step,
maxX = Math.Round((graph.MaxVisibleGraph.x + 1) / step) * step,
minY = Math.Round((graph.MinVisibleGraph.y - 1) / step) * step,
maxY = Math.Round((graph.MaxVisibleGraph.y + 1) / step) * step;
for (double x = minX; x < maxX; x += step)
{
for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1 / detail)
for (double y = minY; y < maxY; y += step)
{
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)
protected GraphLine MakeSlopeLine(Float2 position, double slope)
{
double size = detail;
double size = _detail;
double dirX = size, dirY = slope * size;
double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY);
@ -46,6 +74,85 @@ public class SlopeField : Graphable
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 ShallowCopy() => new SlopeField(_detail, equ);
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 IEnumerable<IGraphPart> GetSelectionItemsToRender(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
[
new GraphUiText($"M = {slope:0.000}", point, ContentAlignment.BottomLeft),
new GraphUiCircle(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);

View File

@ -0,0 +1,152 @@
using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables;
public class TangentLine : Graphable, IConvertEquation, ITranslatableX
{
public bool UngraphWhenConvertedToEquation => true;
public double Position
{
get => _position;
set
{
currentSlope = DerivativeAtPoint(value);
_position = value;
}
}
private double _position; // Private because it has exactly the same functionality as `Position`.
public double OffsetX
{
get => Position;
set => Position = value;
}
protected readonly Equation parent;
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 = [];
this.length = length;
this.parent = parent;
Position = position;
parent.OnInvalidate += (graph) =>
{
// I don't love this but it works.
EraseCache();
Position = _position; // Done for side effects.
};
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
Float2 point = new(Position, currentSlope.y);
return
[
MakeSlopeLine(),
new GraphUiCircle(point)
];
}
protected GraphLine MakeSlopeLine()
{
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 + dirX, currentSlope.y + dirY), new(Position - dirX, currentSlope.y - dirY));
}
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;
double initial = parent.GetValueAt(x);
Float2 result = new((parent.GetValueAt(x + step) - initial) / step, initial);
slopeCache.Add(x, result);
return result;
}
public override Graphable ShallowCopy() => new TangentLine(length, Position, parent);
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 IEnumerable<IGraphPart> GetSelectionItemsToRender(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;
double slope = currentSlope.x;
Float2 point = new(lineX, lineY);
return
[
new GraphUiText($"M = {slope:0.000}", point, ContentAlignment.BottomLeft),
new GraphUiCircle(new(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);
}
public Equation ToEquation()
{
double slope = currentSlope.x, x1 = Position, y1 = currentSlope.y;
return new(x => slope * (x - x1) + y1)
{
Name = Name,
Color = Color
};
}
}

9
Base/IGraphPart.cs Normal file
View File

@ -0,0 +1,9 @@
using Graphing.Forms;
using System.Drawing;
namespace Graphing;
public interface IGraphPart
{
public void Render(in GraphForm form, in Graphics g, in Pen pen);
}

View File

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

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/GraphLine.cs Normal file
View File

@ -0,0 +1,31 @@
using Graphing.Forms;
using System.Drawing;
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 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);
g.DrawLine(pen, start, end);
}
}

View File

@ -0,0 +1,53 @@
using Graphing.Forms;
using System.Drawing;
namespace Graphing.Parts;
public record struct GraphRectangle : IGraphPart
{
public Float2 min, max;
public double opacity;
public GraphRectangle()
{
min = new();
max = new();
opacity = 1;
}
public static GraphRectangle FromSize(Float2 center, Float2 size, double opacity = 1) => 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),
opacity = opacity,
};
public static GraphRectangle FromRange(Float2 min, Float2 max, double opacity = 1) => new()
{
min = min,
max = max,
opacity = opacity,
};
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;
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;
Color initialColor = pen.Color;
pen.Color = Color.FromArgb((int)(opacity * 255), pen.Color);
g.FillRectangle(pen.Brush, new Rectangle(start.x, end.y, size.x, size.y));
pen.Color = initialColor;
}
}

View File

@ -0,0 +1,34 @@
using Graphing.Forms;
using System.Drawing;
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 = 8)
{
this.center = center;
this.radius = radius;
}
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(pen.Brush, new Rectangle(new Point(centerPix.x - rad,
centerPix.y - rad),
new Size(rad * 2, rad * 2)));
}
}

87
Base/Parts/GraphUiText.cs Normal file
View File

@ -0,0 +1,87 @@
using Graphing.Forms;
using System.Drawing;
namespace Graphing.Parts;
public record struct GraphUiText : IGraphPart
{
public string text;
public Float2 position;
public bool background;
public ContentAlignment alignment;
public Int2 offsetPix;
private readonly Font font;
private readonly Brush? backgroundBrush;
public GraphUiText(string text, Float2 position, ContentAlignment alignment,
bool background = true, Int2? offsetPix = null)
{
font = new Font("Segoe UI", 8, FontStyle.Bold);
this.text = text;
this.position = position;
this.background = background;
this.alignment = alignment;
this.offsetPix = offsetPix ?? new();
if (background) backgroundBrush = new SolidBrush(GraphForm.BackgroundColor);
}
public readonly void Render(in GraphForm form, in Graphics g, in Pen p)
{
Int2 posScreen = form.GraphSpaceToScreenSpace(position);
SizeF size = g.MeasureString(text, font);
// Adjust X position based on alignment.
switch (alignment)
{
case ContentAlignment.TopLeft or
ContentAlignment.MiddleLeft or
ContentAlignment.BottomLeft: break; // Nothing to offset.
case ContentAlignment.TopCenter or
ContentAlignment.MiddleCenter or
ContentAlignment.BottomCenter:
posScreen.x -= (int)(size.Width / 2);
break;
case ContentAlignment.TopRight or
ContentAlignment.MiddleRight or
ContentAlignment.BottomRight:
posScreen.x -= (int)size.Width;
break;
}
// Adjust Y position based on alignment.
switch (alignment)
{
case ContentAlignment.TopLeft or
ContentAlignment.TopCenter or
ContentAlignment.TopRight: break; // Nothing to offset.
case ContentAlignment.MiddleLeft or
ContentAlignment.MiddleCenter or
ContentAlignment.MiddleRight:
posScreen.y -= (int)(size.Height / 2);
break;
case ContentAlignment.BottomLeft or
ContentAlignment.BottomCenter or
ContentAlignment.BottomRight:
posScreen.y -= (int)size.Height;
break;
}
posScreen.x += (int)(offsetPix.x * form.DpiFloat / 192);
posScreen.y += (int)(offsetPix.y * form.DpiFloat / 192);
if (background)
{
g.FillRectangle(backgroundBrush!, new Rectangle(posScreen.x, posScreen.y,
(int)size.Width, (int)size.Height));
}
g.DrawString(text, font, p.Brush, new Point(posScreen.x, posScreen.y));
}
}

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-20T12:48:45.8740885Z;True|2024-03-20T08:48:35.6948867-04:00;True|2024-03-20T08:39:01.6402921-04:00;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>

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

@ -3,9 +3,15 @@
This is a graphing calculator I made initially for a Calculus project in a day or so. I've written a basic rendering system in Windows Forms that runs on .NET 8.0.
Currently, it doesn't have a whole lot of features, but I'll be adding more in the future. Here's currently what it can do:
- Graph an equation (duh).
- Graph standard equations (duh).
- There are currently some rendering issues with asymptotes which will be focused on at some point.
- Graph parametric equations.
- 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 +76,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"
});
```

View File

@ -1,5 +1,7 @@
using Graphing.Forms;
using Graphing.Graphables;
using System;
using System.Windows.Forms;
namespace Graphing.Testing;
@ -13,7 +15,13 @@ internal static class Program
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
GraphForm graph = new("One Of The Graphing Calculators Of All Time");
graph.Graph(new Equation(Math.Cos));
Equation equA = new(Math.Sin),
equB = new(Math.Cos);
EquationDifference diff = new(2, equA, equB);
ParametricEquation equC = new(0, 20, t => 0.0375 * t * Math.Cos(t), t => 0.0625 * t * Math.Sin(t) + 3);
TangentLine tanA = new(2, 2, equA);
graph.Graph(equA, equB, diff, equC, equB.ToColumnTable(-3, 3, 2), tanA);
Application.Run(graph);
}

View File

@ -1,15 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<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>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputType>Exe</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputType>WinExe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
</ItemGroup>