Version 1.3 is ready.

Changelog written, release coming out soon.
This commit is contained in:
That_One_Nerd 2024-05-03 09:08:09 -04:00 committed by GitHub
commit ea608656db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2521 additions and 348 deletions

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,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

@ -12,18 +12,18 @@
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ThatOneNerd.Graphing</PackageId>
<Title>ThatOneNerd.Graphing</Title>
<Version>1.2.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;slope field;slopefield;equation;visualizer</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.2.0</PackageReleaseNotes>
https://github.com/That-One-Nerd/Graphing/releases/tag/1.3.0</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -16,6 +16,12 @@
<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>

View File

@ -38,33 +38,45 @@ namespace Graphing.Forms
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, MenuMisc });
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);
@ -81,55 +93,93 @@ namespace Graphing.Forms
// 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
//
@ -141,29 +191,87 @@ namespace Graphing.Forms
// MenuMiscCaches
//
MenuMiscCaches.Name = "MenuMiscCaches";
MenuMiscCaches.Size = new Size(359, 44);
MenuMiscCaches.Size = new Size(299, 44);
MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click;
//
// MiscMenuPreload
//
MiscMenuPreload.Name = "MiscMenuPreload";
MiscMenuPreload.Size = new Size(359, 44);
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();
}
@ -172,17 +280,28 @@ namespace Graphing.Forms
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,46 +1,71 @@
using Graphing.Abstract;
using Graphing.Parts;
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 static readonly Color BackgroundColor = Color.White;
public static readonly Color MainAxisColor = Color.Black;
public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999));
public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0));
public static readonly Color 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 Float2 ScreenCenter { get; private set; }
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 float DpiFloat { get; private set; }
public double ZoomLevel
public Float2 ZoomLevel
{
get => _zoomLevel;
set
{
double oldZoom = ZoomLevel;
_zoomLevel = Math.Clamp(value, 1e-5, 1e3);
int totalSegments = 0;
foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count();
if (totalSegments > 10_000)
{
_zoomLevel = oldZoom;
return; // Too many segments, stop.
}
_zoomLevel = new(Math.Clamp(value.x, 1e-5, 1e3),
Math.Clamp(value.y, 1e-5, 1e3));
OnZoomLevelChanged(this, new());
Invalidate(false);
}
}
private double _zoomLevel;
private Float2 _zoomLevel;
public bool ViewportLocked
{
get => _viewportLocked;
set
{
if (value)
{
FormBorderStyle = FormBorderStyle.FixedSingle;
ResetViewportButton.Text = "🔒";
}
else
{
FormBorderStyle = FormBorderStyle.Sizable;
ResetViewportButton.Text = "🏠";
}
MaximizeBox = !value;
ResetViewportButton.Enabled = !value;
_viewportLocked = value;
}
}
private bool _viewportLocked;
private readonly Point initialWindowPos;
private readonly Size initialWindowSize;
@ -52,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);
@ -68,9 +95,11 @@ public partial class GraphForm : Form
DpiFloat = (float)((Dpi.x + Dpi.y) / 2);
ables = [];
ZoomLevel = 1;
ZoomLevel = new(1, 1);
initialWindowPos = Location;
initialWindowSize = Size;
RunUpdateChecker();
}
public Int2 GraphSpaceToScreenSpace(Float2 graphPoint)
@ -80,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;
@ -95,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;
@ -108,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(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));
@ -131,13 +161,13 @@ public partial class GraphForm : Form
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));
@ -158,14 +188,15 @@ public partial class GraphForm : Form
}
protected virtual void PaintUnits(Graphics g)
{
double axisScale = Math.Pow(2, Math.Round(Math.Log(ZoomLevel, 2)));
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 / axisScale) * axisScale; x <= MaxVisibleGraph.x; x += axisScale)
for (double x = Math.Ceiling(MinVisibleGraph.x / axisScaleX) * axisScaleX; x <= MaxVisibleGraph.x; x += axisScaleX)
{
if (x == 0) x = 0; // Fixes -0
@ -179,7 +210,7 @@ public partial class GraphForm : Form
// Y-axis
int minY = (int)(DpiFloat * 10 / 192);
for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= MaxVisibleGraph.y; y += axisScale)
for (double y = Math.Ceiling(MinVisibleGraph.y / axisScaleY) * axisScaleY; y <= MaxVisibleGraph.y; y += axisScaleY)
{
if (y == 0) continue;
@ -200,7 +231,7 @@ public partial class GraphForm : Form
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);
@ -223,29 +254,31 @@ public partial class GraphForm : Form
// Equation selection detection.
// This system lets you select multiple graphs, and that's cool by me.
if (ableDrag)
if (selectState == SelectionState.GraphSelect)
{
Font textFont = new(Font.Name, 8, FontStyle.Bold);
for (int i = 0; i < ables.Count; i++)
{
if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5))
{
Float2 selectedPoint = ables[i].GetSelectedPoint(this, graphMousePos);
GraphUiCircle select = new(selectedPoint, 8);
Int2 textPos = GraphSpaceToScreenSpace(select.center);
textPos.y -= (int)(DpiFloat * 32 / 192);
string content = $"({selectedPoint.x:0.00}, {selectedPoint.y:0.00})";
SizeF textSize = g.MeasureString(content, textFont);
g.FillRectangle(background, new Rectangle(textPos.x, textPos.y,
(int)textSize.Width, (int)textSize.Height));
g.DrawString(content, textFont, graphPens[i].Brush, new Point(textPos.x, textPos.y));
select.Render(this, g, graphPens[i]);
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);
}
@ -255,77 +288,152 @@ public partial class GraphForm : Form
Invalidate(false);
}
public void Graph(params Graphable[] able)
public void Graph(params Graphable[] newAbles)
{
ables.AddRange(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 bool ableDrag = false;
private Float2 boxSelectA, boxSelectB;
protected override void OnMouseDown(MouseEventArgs e)
{
if (!mouseDrag)
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)) ableDrag = true;
if (able.ShouldSelectGraphable(this, graphMousePos, 1))
selectState = SelectionState.GraphSelect;
}
if (ableDrag) Invalidate(false);
if (selectState == SelectionState.GraphSelect) Invalidate(false);
}
if (!ableDrag)
if (selectState == SelectionState.None && !ViewportLocked)
{
mouseDrag = true;
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);
}
mouseDrag = false;
ableDrag = 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 (ableDrag) 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)
{
@ -346,11 +454,46 @@ public partial class GraphForm : Form
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)
{
@ -360,7 +503,26 @@ public partial class GraphForm : Form
Text = able.Name
};
colorItem.Click += (o, e) => GraphColorPickerButton_Click(able);
MenuColors.DropDownItems.Add(colorItem);
MenuElementsColors.DropDownItems.Add(colorItem);
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)
{
@ -370,7 +532,7 @@ public partial class GraphForm : Form
Text = able.Name
};
derivativeItem.Click += (o, e) => Graph(derivable.Derive());
MenuEquationsDerivative.DropDownItems.Add(derivativeItem);
MenuOperationsDerivative.DropDownItems.Add(derivativeItem);
}
if (able is IIntegrable integrable)
{
@ -380,20 +542,76 @@ public partial class GraphForm : Form
Text = able.Name
};
integralItem.Click += (o, e) => Graph(integrable.Integrate());
MenuEquationsIntegral.DropDownItems.Add(integralItem);
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)
{
@ -402,7 +620,7 @@ public partial class GraphForm : Form
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)
@ -412,12 +630,30 @@ public partial class GraphForm : Form
WindowState = FormWindowState.Normal;
}
public void ResetAllViewport()
{
ScreenCenter = new Float2(0, 0);
ZoomLevel = new(1, 1);
Location = initialWindowPos;
Size = initialWindowSize;
WindowState = FormWindowState.Normal;
Invalidate(false);
}
private ViewCacheForm? cacheForm;
private void MenuMiscCaches_Click(object? sender, EventArgs e)
{
if (this.cacheForm is not null)
{
this.cacheForm.Focus();
return;
}
ViewCacheForm cacheForm = new(this)
{
StartPosition = FormStartPosition.Manual
};
this.cacheForm = cacheForm;
cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2);
@ -429,7 +665,7 @@ public partial class GraphForm : Form
cacheForm.TopMost = true;
cacheForm.Show();
}
private void MiscMenuPreload_Click(object sender, EventArgs e)
private void MiscMenuPreload_Click(object? sender, EventArgs e)
{
Float2 min = MinVisibleGraph, max = MaxVisibleGraph;
Float2 add = new(max.x - min.x, max.y - min.y);
@ -446,4 +682,95 @@ public partial class GraphForm : Form
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 enum SelectionState
{
None = 0,
ViewportDrag,
GraphSelect,
ZoomBox,
}
}

View File

@ -1,7 +1,4 @@
using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms
namespace Graphing.Forms
{
partial class SetZoomForm
{
@ -31,90 +28,157 @@ namespace Graphing.Forms
/// </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

@ -5,118 +5,223 @@ 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);
this.form = form;
}
protected override void OnPaint(PaintEventArgs e)
{
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
ZoomMinValue.Text = minZoomRange.ToString("0.00");
ValueLabel.Text = $"{zoomLevel:0.00}x";
base.OnPaint(e);
form.ZoomLevel = 1 / zoomLevel;
form.Invalidate(false);
}
private double FactorToZoom(double factor)
{
return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange);
}
private double ZoomToFactor(double zoom)
{
double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange);
return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue));
}
private void ZoomTrackBar_Scroll(object? sender, EventArgs e)
{
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
zoomLevel = FactorToZoom(factor);
Invalidate(true);
}
private void ZoomMinValue_TextChanged(object? sender, EventArgs e)
{
double original = minZoomRange;
try
MinBoxX.Leave += MinBoxX_Finish;
MinBoxX.KeyDown += (o, e) =>
{
double value;
if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) ||
ZoomMinValue.Text.EndsWith('.'))
{
return;
}
else
{
value = double.Parse(ZoomMinValue.Text);
if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new();
}
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);
};
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);
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);
};
}
zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
private void EnableBoxSelect_Click(object? sender, EventArgs e)
{
boxSelectEnabled = !boxSelectEnabled;
refForm.canBoxSelect = boxSelectEnabled;
if (boxSelectEnabled)
{
EnableBoxSelect.Text = $"Cancel ...";
refForm.Focus();
}
catch
else
{
minZoomRange = original;
ZoomMinValue.Text = minZoomRange.ToString("0.00");
EnableBoxSelect.Text = "Box Select";
}
}
private void ZoomMaxValue_TextChanged(object sender, EventArgs e)
private void MatchAspectButton_Click(object? sender, EventArgs e)
{
double original = maxZoomRange;
try
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)
{
double value;
if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) ||
ZoomMaxValue.Text.EndsWith('.'))
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);
}
refForm.Width = newWidth;
}
private void NormalizeButton_Click(object? sender, EventArgs e)
{
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 MinBoxX_Finish(object? sender, EventArgs e)
{
if (double.TryParse(MinBoxX.Text, out double minX))
{
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)
{
if (double.TryParse(MaxBoxX.Text, out double maxX))
{
maxZoomRange = original;
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
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>

View File

@ -33,6 +33,7 @@ public partial class ViewCacheForm : Form
foreach (Graphable able in refForm.Graphables)
{
long thisBytes = able.GetCacheBytes();
if (thisBytes == 0) continue;
CachePie.Values.Add((able.Color, thisBytes));
totalBytes += thisBytes;

View File

@ -30,12 +30,12 @@ public abstract class Graphable
public abstract IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph);
public abstract Graphable DeepCopy();
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 Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => default;
public virtual IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) => [];
}

View File

@ -2,6 +2,7 @@
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
namespace Graphing.Graphables;
@ -23,6 +24,7 @@ public class ColumnTable : Graphable
}
public ColumnTable(double step, Equation equation, double min, double max)
{
Color = equation.Color;
Name = $"Column Table for {equation.Name}";
tableXY = [];
@ -37,7 +39,7 @@ public class ColumnTable : Graphable
public override long GetCacheBytes() => 16 * tableXY.Count;
public override Graphable DeepCopy() => new ColumnTable(width / 0.75, tableXY.ToArray().ToDictionary());
public override Graphable ShallowCopy() => new ColumnTable(width / 0.75, tableXY);
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
@ -45,12 +47,87 @@ public class ColumnTable : Graphable
foreach (KeyValuePair<double, double> col in tableXY)
{
items.Add(GraphRectangle.FromSize(new Float2(col.Key, col.Value / 2),
new Float2(width, col.Value)));
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

@ -3,16 +3,26 @@ using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables;
public class Equation : Graphable, IIntegrable, IDerivable
public class Equation : Graphable, IIntegrable, IDerivable, ITranslatableXY, IConvertSlopeField,
IConvertColumnTable
{
private static int equationNum;
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)
{
equationNum++;
@ -20,6 +30,11 @@ public class Equation : Graphable, IIntegrable, IDerivable
this.equ = equ;
cache = [];
OffsetX = 0;
OffsetY = 0;
OnInvalidate = delegate { };
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
@ -46,31 +61,44 @@ public class Equation : Graphable, IIntegrable, IDerivable
previousX = currentX;
previousY = currentY;
}
OnInvalidate.Invoke(graph);
return lines;
}
public Graphable Derive() => new Equation(x =>
protected double DerivativeAtPoint(double x)
{
const double step = 1e-3;
return (equ(x + step) - equ(x)) / step;
});
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);
if (dist < epsilon) return nearest;
(double dist, double nearest, int index) = NearestCachedPoint(x - OffsetX);
if (dist < epsilon) return nearest + OffsetY;
else
{
double result = equ(x);
cache.Insert(index + 1, new(x, result));
return result;
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);
@ -103,7 +131,7 @@ public class Equation : Graphable, IIntegrable, IDerivable
}
}
public override Graphable DeepCopy() => new Equation(equ);
public override Graphable ShallowCopy() => new Equation(equ);
public override long GetCacheBytes() => cache.Count * 16;
@ -121,8 +149,15 @@ public class Equation : Graphable, IIntegrable, IDerivable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) =>
new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3));
public override 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)
{

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

@ -47,7 +47,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
usingAlt = true;
}
public override Graphable DeepCopy() => new IntegralEquation(this);
public override Graphable ShallowCopy() => new IntegralEquation(this);
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
@ -164,7 +164,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
}
else
{
stepY += baseEquDel!(stepX) * dX;
stepY += (baseEquDel!(stepX - baseEqu!.OffsetX) + baseEqu.OffsetY) * dX;
}
}
@ -178,8 +178,8 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
public Graphable Derive()
{
if (usingAlt) return altBaseEqu!.DeepCopy();
else return (Equation)baseEqu!.DeepCopy();
if (usingAlt) return altBaseEqu!.ShallowCopy();
else return (Equation)baseEqu!.ShallowCopy();
}
public Graphable Integrate() => new IntegralEquation(this);
@ -234,6 +234,6 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) =>
new(graphMousePos.x, IntegralAtPoint(graphMousePos.x));
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

@ -2,6 +2,7 @@
using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables;
@ -9,29 +10,50 @@ public class SlopeField : Graphable
{
private static int slopeFieldNum;
protected readonly SlopeFieldsDelegate equ;
protected readonly int 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;
protected readonly SlopeFieldsDelegate equ;
protected readonly List<(Float2, GraphLine)> cache;
public SlopeField(int detail, SlopeFieldsDelegate equ)
public SlopeField(double detail, SlopeFieldsDelegate equ)
{
slopeFieldNum++;
Name = $"Slope Field {slopeFieldNum}";
this.equ = equ;
this.detail = detail;
_detail = detail;
cache = [];
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
double epsilon = 1 / (detail * 2.0);
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.0 / 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.0 / detail)
for (double y = minY; y < maxY; y += step)
{
lines.Add(GetFromCache(epsilon, x, y));
}
@ -42,7 +64,7 @@ public class SlopeField : Graphable
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);
@ -72,17 +94,17 @@ public class SlopeField : Graphable
return result;
}
public override Graphable DeepCopy() => new SlopeField(detail, equ);
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);
Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail,
Math.Round(graphMousePos.y * _detail) / _detail);
double epsilon = 1 / (detail * 2.0);
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);
@ -101,12 +123,12 @@ public class SlopeField : Graphable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos)
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);
Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail,
Math.Round(graphMousePos.y * _detail) / _detail);
double epsilon = 1 / (detail * 2.0);
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);
@ -114,14 +136,18 @@ public class SlopeField : Graphable
lineY = slope * (lineX - nearestPos.x) + nearestPos.y;
Float2 point = new(lineX, lineY);
return point;
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 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)
for (double y = Math.Ceiling(yRange.x - 1); y < yRange.y + 1; y += 1.0 / _detail)
{
GetFromCache(step, x, y);
}

View File

@ -1,12 +1,16 @@
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 TangentLine : Graphable
public class TangentLine : Graphable, IConvertEquation, ITranslatableX
{
public bool UngraphWhenConvertedToEquation => true;
public double Position
{
get => _position;
@ -18,8 +22,13 @@ public class TangentLine : Graphable
}
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 EquationDelegate parentEqu;
protected readonly double length;
@ -35,16 +44,26 @@ public class TangentLine : Graphable
Name = $"Tangent Line of {parent.Name}";
slopeCache = [];
parentEqu = parent.GetDelegate();
Position = position;
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, 8)];
return
[
MakeSlopeLine(),
new GraphUiCircle(point)
];
}
protected GraphLine MakeSlopeLine()
{
@ -63,13 +82,13 @@ public class TangentLine : Graphable
const double step = 1e-3;
double initial = parentEqu(x);
Float2 result = new((parentEqu(x + step) - initial) / step, initial);
double initial = parent.GetValueAt(x);
Float2 result = new((parent.GetValueAt(x + step) - initial) / step, initial);
slopeCache.Add(x, result);
return result;
}
public override Graphable DeepCopy() => new TangentLine(length, Position, parent);
public override Graphable ShallowCopy() => new TangentLine(length, Position, parent);
public override void EraseCache() => slopeCache.Clear();
public override long GetCacheBytes() => slopeCache.Count * 24;
@ -93,7 +112,7 @@ public class TangentLine : Graphable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos)
public override IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
{
GraphLine line = MakeSlopeLine();
@ -101,7 +120,15 @@ public class TangentLine : Graphable
Math.Min(line.a.x, line.b.x),
Math.Max(line.a.x, line.b.x)),
lineY = currentSlope.x * (lineX - Position) + currentSlope.y;
return new Float2(lineX, lineY);
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)
@ -112,4 +139,14 @@ public class TangentLine : Graphable
// 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
};
}
}

View File

@ -6,24 +6,28 @@ 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) => new()
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)
center.y + size.y / 2),
opacity = opacity,
};
public static GraphRectangle FromRange(Float2 min, Float2 max) => new()
public static GraphRectangle FromRange(Float2 min, Float2 max, double opacity = 1) => new()
{
min = min,
max = max
max = max,
opacity = opacity,
};
public void Render(in GraphForm form, in Graphics g, in Pen pen)
@ -41,6 +45,9 @@ public record struct GraphRectangle : IGraphPart
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

@ -13,7 +13,7 @@ public record struct GraphUiCircle : IGraphPart
center = new();
radius = 1;
}
public GraphUiCircle(Float2 center, int radius)
public GraphUiCircle(Float2 center, int radius = 8)
{
this.center = center;
this.radius = radius;

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

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<History>True|2024-03-20T12:39:01.6402921Z;True|2024-03-13T10:31:43.4569441-04:00;False|2024-03-13T10:30:01.4347009-04:00;False|2024-03-13T10:27:31.9554551-04:00;</History>
<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

@ -3,8 +3,9 @@
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.

View File

@ -13,18 +13,15 @@ internal static class Program
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
GraphForm graph = new("One Of The Graphing Calculators Of All Time");
Equation equ = new(Math.Sin);
SlopeField sf = new(2, (x, y) => Math.Cos(x));
TangentLine tl = new(2, 2, equ);
graph.Graph(equ, sf, tl);
// Now, when integrating equations, the result is much less jagged
// and much faster. Try it out! You can also select points along
// equations and such as well. Click on an equation to see for
// yourself!
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);
}