Version 1.3 is ready. #41

Merged
That-One-Nerd merged 23 commits from canary into main 2024-05-03 09:08:09 -04:00
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> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ThatOneNerd.Graphing</PackageId> <PackageId>ThatOneNerd.Graphing</PackageId>
<Title>ThatOneNerd.Graphing</Title> <Title>ThatOneNerd.Graphing</Title>
<Version>1.2.0</Version> <Version>1.3.0</Version>
<Authors>That_One_Nerd</Authors> <Authors>That_One_Nerd</Authors>
<Description>A fairly adept graphing calculator made in Windows Forms.</Description> <Description>A fairly adept graphing calculator made in Windows Forms.</Description>
<Copyright>MIT</Copyright> <Copyright>MIT</Copyright>
<RepositoryUrl>https://github.com/That-One-Nerd/Graphing</RepositoryUrl> <RepositoryUrl>https://github.com/That-One-Nerd/Graphing</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<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> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>True</IncludeSymbols> <IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageReleaseNotes>View the GitHub release for the changelog: <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>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -16,6 +16,12 @@
<Compile Update="Forms\SetZoomForm.cs"> <Compile Update="Forms\SetZoomForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Update="Forms\SlopeFieldDetailForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Forms\TranslateForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Forms\ViewCacheForm.cs"> <Compile Update="Forms\ViewCacheForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>

View File

@ -38,33 +38,45 @@ namespace Graphing.Forms
ButtonViewportSetCenter = new ToolStripMenuItem(); ButtonViewportSetCenter = new ToolStripMenuItem();
ButtonViewportReset = new ToolStripMenuItem(); ButtonViewportReset = new ToolStripMenuItem();
ButtonViewportResetWindow = new ToolStripMenuItem(); ButtonViewportResetWindow = new ToolStripMenuItem();
MenuColors = new ToolStripMenuItem(); MenuElements = new ToolStripMenuItem();
MenuEquations = new ToolStripMenuItem(); MenuElementsColors = new ToolStripMenuItem();
MenuEquationsDerivative = new ToolStripMenuItem(); MenuElementsRemove = new ToolStripMenuItem();
MenuEquationsIntegral = 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(); MenuMisc = new ToolStripMenuItem();
MenuMiscCaches = new ToolStripMenuItem(); MenuMiscCaches = new ToolStripMenuItem();
MiscMenuPreload = new ToolStripMenuItem(); MiscMenuPreload = new ToolStripMenuItem();
UpdaterPopup = new Panel();
UpdaterPopupDownloadButton = new Button();
UpdaterPopupCloseButton = new Button();
UpdaterPopupMessage = new Label();
MenuElementsDetail = new ToolStripMenuItem();
GraphMenu.SuspendLayout(); GraphMenu.SuspendLayout();
UpdaterPopup.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
// ResetViewportButton // ResetViewportButton
// //
ResetViewportButton.Anchor = AnchorStyles.Top | AnchorStyles.Right; ResetViewportButton.Anchor = AnchorStyles.Top | AnchorStyles.Right;
ResetViewportButton.Font = new Font("Segoe UI Emoji", 13.875F, FontStyle.Regular, GraphicsUnit.Point, 0); ResetViewportButton.Font = new Font("Segoe UI Emoji", 12F, FontStyle.Regular, GraphicsUnit.Point, 0);
ResetViewportButton.Location = new Point(1373, 43); ResetViewportButton.Location = new Point(1372, 43);
ResetViewportButton.Margin = new Padding(4, 2, 4, 2);
ResetViewportButton.Name = "ResetViewportButton"; ResetViewportButton.Name = "ResetViewportButton";
ResetViewportButton.Size = new Size(64, 64); ResetViewportButton.Size = new Size(63, 64);
ResetViewportButton.TabIndex = 0; ResetViewportButton.TabIndex = 0;
ResetViewportButton.Text = "⌂"; ResetViewportButton.Text = "🏠";
ResetViewportButton.TextAlign = ContentAlignment.TopRight;
ResetViewportButton.UseVisualStyleBackColor = true; ResetViewportButton.UseVisualStyleBackColor = true;
ResetViewportButton.Click += ResetViewportButton_Click; ResetViewportButton.Click += ResetViewportButton_Click;
// //
// GraphMenu // GraphMenu
// //
GraphMenu.ImageScalingSize = new Size(32, 32); 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.Location = new Point(0, 0);
GraphMenu.Name = "GraphMenu"; GraphMenu.Name = "GraphMenu";
GraphMenu.Size = new Size(1449, 42); GraphMenu.Size = new Size(1449, 42);
@ -81,55 +93,93 @@ namespace Graphing.Forms
// ButtonViewportSetZoom // ButtonViewportSetZoom
// //
ButtonViewportSetZoom.Name = "ButtonViewportSetZoom"; ButtonViewportSetZoom.Name = "ButtonViewportSetZoom";
ButtonViewportSetZoom.Size = new Size(350, 44); ButtonViewportSetZoom.Size = new Size(359, 44);
ButtonViewportSetZoom.Text = "Set Zoom"; ButtonViewportSetZoom.Text = "Set Zoom";
ButtonViewportSetZoom.Click += ButtonViewportSetZoom_Click; ButtonViewportSetZoom.Click += ButtonViewportSetZoom_Click;
// //
// ButtonViewportSetCenter // ButtonViewportSetCenter
// //
ButtonViewportSetCenter.Name = "ButtonViewportSetCenter"; ButtonViewportSetCenter.Name = "ButtonViewportSetCenter";
ButtonViewportSetCenter.Size = new Size(350, 44); ButtonViewportSetCenter.Size = new Size(359, 44);
ButtonViewportSetCenter.Text = "Set Center Position"; ButtonViewportSetCenter.Text = "Set Center Position";
ButtonViewportSetCenter.Click += ButtonViewportSetCenter_Click; ButtonViewportSetCenter.Click += ButtonViewportSetCenter_Click;
// //
// ButtonViewportReset // ButtonViewportReset
// //
ButtonViewportReset.Name = "ButtonViewportReset"; ButtonViewportReset.Name = "ButtonViewportReset";
ButtonViewportReset.Size = new Size(350, 44); ButtonViewportReset.Size = new Size(359, 44);
ButtonViewportReset.Text = "Reset Viewport"; ButtonViewportReset.Text = "Reset Viewport";
ButtonViewportReset.Click += ButtonViewportReset_Click; ButtonViewportReset.Click += ButtonViewportReset_Click;
// //
// ButtonViewportResetWindow // ButtonViewportResetWindow
// //
ButtonViewportResetWindow.Name = "ButtonViewportResetWindow"; ButtonViewportResetWindow.Name = "ButtonViewportResetWindow";
ButtonViewportResetWindow.Size = new Size(350, 44); ButtonViewportResetWindow.Size = new Size(359, 44);
ButtonViewportResetWindow.Text = "Reset Window Size"; ButtonViewportResetWindow.Text = "Reset Window Size";
ButtonViewportResetWindow.Click += ButtonViewportResetWindow_Click; ButtonViewportResetWindow.Click += ButtonViewportResetWindow_Click;
// //
// MenuColors // MenuElements
// //
MenuColors.Name = "MenuColors"; MenuElements.DropDownItems.AddRange(new ToolStripItem[] { MenuElementsColors, MenuElementsDetail, MenuElementsRemove });
MenuColors.Size = new Size(101, 38); MenuElements.Name = "MenuElements";
MenuColors.Text = "Colors"; MenuElements.Size = new Size(131, 38);
MenuElements.Text = "Elements";
// //
// MenuEquations // MenuElementsColors
// //
MenuEquations.DropDownItems.AddRange(new ToolStripItem[] { MenuEquationsDerivative, MenuEquationsIntegral }); MenuElementsColors.Name = "MenuElementsColors";
MenuEquations.Name = "MenuEquations"; MenuElementsColors.Size = new Size(359, 44);
MenuEquations.Size = new Size(138, 38); MenuElementsColors.Text = "Colors";
MenuEquations.Text = "Equations";
// //
// MenuEquationsDerivative // MenuElementsRemove
// //
MenuEquationsDerivative.Name = "MenuEquationsDerivative"; MenuElementsRemove.Name = "MenuElementsRemove";
MenuEquationsDerivative.Size = new Size(360, 44); MenuElementsRemove.Size = new Size(359, 44);
MenuEquationsDerivative.Text = "Compute Derivative"; MenuElementsRemove.Text = "Remove";
// //
// MenuEquationsIntegral // MenuOperations
// //
MenuEquationsIntegral.Name = "MenuEquationsIntegral"; MenuOperations.DropDownItems.AddRange(new ToolStripItem[] { MenuOperationsDerivative, MenuOperationsIntegral, MenuOperationsTranslate });
MenuEquationsIntegral.Size = new Size(360, 44); MenuOperations.Name = "MenuOperations";
MenuEquationsIntegral.Text = "Compute Integral"; MenuOperations.Size = new Size(151, 38);
MenuOperations.Text = "Operations";
//
// MenuOperationsDerivative
//
MenuOperationsDerivative.Name = "MenuOperationsDerivative";
MenuOperationsDerivative.Size = new Size(360, 44);
MenuOperationsDerivative.Text = "Compute Derivative";
//
// MenuOperationsIntegral
//
MenuOperationsIntegral.Name = "MenuOperationsIntegral";
MenuOperationsIntegral.Size = new Size(360, 44);
MenuOperationsIntegral.Text = "Compute Integral";
//
// MenuOperationsTranslate
//
MenuOperationsTranslate.Name = "MenuOperationsTranslate";
MenuOperationsTranslate.Size = new Size(360, 44);
MenuOperationsTranslate.Text = "Translate";
//
// MenuConvert
//
MenuConvert.DropDownItems.AddRange(new ToolStripItem[] { MenuConvertEquation, MenuConvertSlopeField });
MenuConvert.Name = "MenuConvert";
MenuConvert.Size = new Size(118, 38);
MenuConvert.Text = "Convert";
//
// MenuConvertEquation
//
MenuConvertEquation.Name = "MenuConvertEquation";
MenuConvertEquation.Size = new Size(297, 44);
MenuConvertEquation.Text = "To Equation";
//
// MenuConvertSlopeField
//
MenuConvertSlopeField.Name = "MenuConvertSlopeField";
MenuConvertSlopeField.Size = new Size(297, 44);
MenuConvertSlopeField.Text = "To Slope Field";
// //
// MenuMisc // MenuMisc
// //
@ -141,29 +191,87 @@ namespace Graphing.Forms
// MenuMiscCaches // MenuMiscCaches
// //
MenuMiscCaches.Name = "MenuMiscCaches"; MenuMiscCaches.Name = "MenuMiscCaches";
MenuMiscCaches.Size = new Size(359, 44); MenuMiscCaches.Size = new Size(299, 44);
MenuMiscCaches.Text = "View Caches"; MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click; MenuMiscCaches.Click += MenuMiscCaches_Click;
// //
// MiscMenuPreload // MiscMenuPreload
// //
MiscMenuPreload.Name = "MiscMenuPreload"; MiscMenuPreload.Name = "MiscMenuPreload";
MiscMenuPreload.Size = new Size(359, 44); MiscMenuPreload.Size = new Size(299, 44);
MiscMenuPreload.Text = "Preload Cache"; MiscMenuPreload.Text = "Preload Cache";
MiscMenuPreload.Click += MiscMenuPreload_Click; 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 // GraphForm
// //
AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1449, 907); ClientSize = new Size(1449, 907);
Controls.Add(UpdaterPopup);
Controls.Add(ResetViewportButton); Controls.Add(ResetViewportButton);
Controls.Add(GraphMenu); Controls.Add(GraphMenu);
MainMenuStrip = GraphMenu; MainMenuStrip = GraphMenu;
Margin = new Padding(4, 2, 4, 2);
Name = "GraphForm"; Name = "GraphForm";
Text = "GraphFormBase"; Text = "GraphFormBase";
GraphMenu.ResumeLayout(false); GraphMenu.ResumeLayout(false);
GraphMenu.PerformLayout(); GraphMenu.PerformLayout();
UpdaterPopup.ResumeLayout(false);
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
} }
@ -172,17 +280,28 @@ namespace Graphing.Forms
private Button ResetViewportButton; private Button ResetViewportButton;
private MenuStrip GraphMenu; private MenuStrip GraphMenu;
private ToolStripMenuItem MenuColors;
private ToolStripMenuItem MenuViewport; private ToolStripMenuItem MenuViewport;
private ToolStripMenuItem ButtonViewportSetZoom; private ToolStripMenuItem ButtonViewportSetZoom;
private ToolStripMenuItem ButtonViewportSetCenter; private ToolStripMenuItem ButtonViewportSetCenter;
private ToolStripMenuItem ButtonViewportReset; private ToolStripMenuItem ButtonViewportReset;
private ToolStripMenuItem ButtonViewportResetWindow; private ToolStripMenuItem ButtonViewportResetWindow;
private ToolStripMenuItem MenuEquations; private ToolStripMenuItem MenuOperations;
private ToolStripMenuItem MenuEquationsDerivative; private ToolStripMenuItem MenuOperationsDerivative;
private ToolStripMenuItem MenuEquationsIntegral; private ToolStripMenuItem MenuOperationsIntegral;
private ToolStripMenuItem MenuMisc; private ToolStripMenuItem MenuMisc;
private ToolStripMenuItem MenuMiscCaches; private ToolStripMenuItem MenuMiscCaches;
private ToolStripMenuItem MiscMenuPreload; 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.Abstract;
using Graphing.Parts; using Graphing.Graphables;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Windows.Forms; using System.Windows.Forms;
namespace Graphing.Forms; namespace Graphing.Forms;
public partial class GraphForm : Form public partial class GraphForm : Form
{ {
public static readonly Color BackgroundColor = Color.White;
public static readonly Color MainAxisColor = Color.Black; public static readonly Color MainAxisColor = Color.Black;
public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); // Grayish
public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0)); public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0)); // Lighter grayish
public static readonly Color UnitsTextColor = Color.Black; 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 Float2 Dpi { get; private set; }
public float DpiFloat { get; private set; } public float DpiFloat { get; private set; }
public double ZoomLevel public Float2 ZoomLevel
{ {
get => _zoomLevel; get => _zoomLevel;
set set
{ {
double oldZoom = ZoomLevel; _zoomLevel = new(Math.Clamp(value.x, 1e-5, 1e3),
Math.Clamp(value.y, 1e-5, 1e3));
OnZoomLevelChanged(this, new());
Invalidate(false);
}
}
private Float2 _zoomLevel;
_zoomLevel = Math.Clamp(value, 1e-5, 1e3); public bool ViewportLocked
int totalSegments = 0;
foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count();
if (totalSegments > 10_000)
{ {
_zoomLevel = oldZoom; get => _viewportLocked;
return; // Too many segments, stop. 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 double _zoomLevel;
private readonly Point initialWindowPos; private readonly Point initialWindowPos;
private readonly Size initialWindowSize; private readonly Size initialWindowSize;
@ -52,6 +77,8 @@ public partial class GraphForm : Form
private readonly List<Graphable> ables; private readonly List<Graphable> ables;
public event EventHandler OnZoomLevelChanged = delegate { };
public GraphForm(string title) public GraphForm(string title)
{ {
SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
@ -68,9 +95,11 @@ public partial class GraphForm : Form
DpiFloat = (float)((Dpi.x + Dpi.y) / 2); DpiFloat = (float)((Dpi.x + Dpi.y) / 2);
ables = []; ables = [];
ZoomLevel = 1; ZoomLevel = new(1, 1);
initialWindowPos = Location; initialWindowPos = Location;
initialWindowSize = Size; initialWindowSize = Size;
RunUpdateChecker();
} }
public Int2 GraphSpaceToScreenSpace(Float2 graphPoint) public Int2 GraphSpaceToScreenSpace(Float2 graphPoint)
@ -80,8 +109,8 @@ public partial class GraphForm : Form
graphPoint.x -= ScreenCenter.x; graphPoint.x -= ScreenCenter.x;
graphPoint.y -= ScreenCenter.y; graphPoint.y -= ScreenCenter.y;
graphPoint.x *= Dpi.x / ZoomLevel; graphPoint.x *= Dpi.x / ZoomLevel.x;
graphPoint.y *= Dpi.y / ZoomLevel; graphPoint.y *= Dpi.y / ZoomLevel.y;
graphPoint.x += ClientRectangle.Width / 2.0; graphPoint.x += ClientRectangle.Width / 2.0;
graphPoint.y += ClientRectangle.Height / 2.0; graphPoint.y += ClientRectangle.Height / 2.0;
@ -95,8 +124,8 @@ public partial class GraphForm : Form
result.x -= ClientRectangle.Width / 2.0; result.x -= ClientRectangle.Width / 2.0;
result.y -= ClientRectangle.Height / 2.0; result.y -= ClientRectangle.Height / 2.0;
result.x /= Dpi.x / ZoomLevel; result.x /= Dpi.x / ZoomLevel.x;
result.y /= Dpi.y / ZoomLevel; result.y /= Dpi.y / ZoomLevel.y;
result.x += ScreenCenter.x; result.x += ScreenCenter.x;
result.y += ScreenCenter.y; result.y += ScreenCenter.y;
@ -108,19 +137,20 @@ public partial class GraphForm : Form
protected virtual void PaintGrid(Graphics g) 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. // Draw horizontal/vertical quarter-axis.
Brush quarterBrush = new SolidBrush(QuarterAxisColor); Brush quarterBrush = new SolidBrush(QuarterAxisColor);
Pen quarterPen = new(quarterBrush, DpiFloat * 2 / 192); 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)), Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)),
endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y)); endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y));
g.DrawLine(quarterPen, startPos, endPos); 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)), Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)),
endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y)); endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y));
@ -131,13 +161,13 @@ public partial class GraphForm : Form
Brush semiBrush = new SolidBrush(SemiAxisColor); Brush semiBrush = new SolidBrush(SemiAxisColor);
Pen semiPen = new(semiBrush, DpiFloat * 2 / 192); 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)), Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)),
endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y)); endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y));
g.DrawLine(semiPen, startPos, endPos); 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)), Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)),
endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y)); endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y));
@ -158,14 +188,15 @@ public partial class GraphForm : Form
} }
protected virtual void PaintUnits(Graphics g) 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); Brush textBrush = new SolidBrush(UnitsTextColor);
Font textFont = new(Font.Name, 9, FontStyle.Regular); Font textFont = new(Font.Name, 9, FontStyle.Regular);
// X-axis // X-axis
int minX = (int)(DpiFloat * 50 / 192), int minX = (int)(DpiFloat * 50 / 192),
maxX = ClientRectangle.Height - (int)(DpiFloat * 40 / 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 if (x == 0) x = 0; // Fixes -0
@ -179,7 +210,7 @@ public partial class GraphForm : Form
// Y-axis // Y-axis
int minY = (int)(DpiFloat * 10 / 192); 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; if (y == 0) continue;
@ -200,7 +231,7 @@ public partial class GraphForm : Form
Graphics g = e.Graphics; Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality; g.SmoothingMode = SmoothingMode.HighQuality;
Brush background = new SolidBrush(Color.White); Brush background = new SolidBrush(BackgroundColor);
g.FillRectangle(background, e.ClipRectangle); g.FillRectangle(background, e.ClipRectangle);
PaintGrid(g); PaintGrid(g);
@ -223,29 +254,31 @@ public partial class GraphForm : Form
// Equation selection detection. // Equation selection detection.
// This system lets you select multiple graphs, and that's cool by me. // 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++) for (int i = 0; i < ables.Count; i++)
{ {
if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5)) if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5))
{ {
Float2 selectedPoint = ables[i].GetSelectedPoint(this, graphMousePos); IEnumerable<IGraphPart> selectionParts = ables[i].GetSelectionItemsToRender(this, graphMousePos);
GraphUiCircle select = new(selectedPoint, 8); foreach (IGraphPart selPart in selectionParts) selPart.Render(this, g, graphPens[i]);
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]);
} }
} }
} }
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); base.OnPaint(e);
} }
@ -255,77 +288,152 @@ public partial class GraphForm : Form
Invalidate(false); 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(); RegenerateMenuItems();
Invalidate(false); 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 Int2 initialMouseLocation;
private Float2 initialScreenCenter; private Float2 initialScreenCenter;
private bool ableDrag = false; private Float2 boxSelectA, boxSelectB;
protected override void OnMouseDown(MouseEventArgs e) 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); Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X, Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y)); clientMousePos.Y));
foreach (Graphable able in Graphables) 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); initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y);
initialScreenCenter = ScreenCenter; initialScreenCenter = ScreenCenter;
} }
} }
protected override void OnMouseUp(MouseEventArgs e) 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, Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X,
initialMouseLocation.y - Cursor.Position.Y); 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, ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y); initialScreenCenter.y + graphDiff.y);
} }
mouseDrag = false; else if (selectState == SelectionState.ZoomBox)
ableDrag = false; {
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); Invalidate(false);
} }
protected override void OnMouseMove(MouseEventArgs e) 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, Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X,
initialMouseLocation.y - Cursor.Position.Y); 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, ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y); 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) 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); Invalidate(false);
} }
private void ResetViewportButton_Click(object? sender, EventArgs e) private void ResetViewportButton_Click(object? sender, EventArgs e)
{ {
ScreenCenter = new Float2(0, 0); ResetAllViewport();
ZoomLevel = 1;
Invalidate(false);
} }
private void GraphColorPickerButton_Click(Graphable able) private void GraphColorPickerButton_Click(Graphable able)
{ {
@ -346,11 +454,46 @@ public partial class GraphForm : Form
RegenerateMenuItems(); 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() private void RegenerateMenuItems()
{ {
MenuColors.DropDownItems.Clear(); MenuElementsColors.DropDownItems.Clear();
MenuEquationsDerivative.DropDownItems.Clear(); MenuElementsDetail.DropDownItems.Clear();
MenuEquationsIntegral.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) foreach (Graphable able in ables)
{ {
@ -360,7 +503,26 @@ public partial class GraphForm : Form
Text = able.Name Text = able.Name
}; };
colorItem.Click += (o, e) => GraphColorPickerButton_Click(able); 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) if (able is IDerivable derivable)
{ {
@ -370,7 +532,7 @@ public partial class GraphForm : Form
Text = able.Name Text = able.Name
}; };
derivativeItem.Click += (o, e) => Graph(derivable.Derive()); derivativeItem.Click += (o, e) => Graph(derivable.Derive());
MenuEquationsDerivative.DropDownItems.Add(derivativeItem); MenuOperationsDerivative.DropDownItems.Add(derivativeItem);
} }
if (able is IIntegrable integrable) if (able is IIntegrable integrable)
{ {
@ -380,20 +542,76 @@ public partial class GraphForm : Form
Text = able.Name Text = able.Name
}; };
integralItem.Click += (o, e) => Graph(integrable.Integrate()); 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) 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, StartPosition = FormStartPosition.Manual,
}; };
picker.Location = new Point(Location.X + ClientRectangle.Width + 10, zoomForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2); Location.Y + (ClientRectangle.Height - zoomForm.ClientRectangle.Height) / 2);
picker.ShowDialog();
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) 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) private void ButtonViewportReset_Click(object? sender, EventArgs e)
{ {
ScreenCenter = new Float2(0, 0); ScreenCenter = new Float2(0, 0);
ZoomLevel = 1; ZoomLevel = new(1, 1);
Invalidate(false); Invalidate(false);
} }
private void ButtonViewportResetWindow_Click(object? sender, EventArgs e) private void ButtonViewportResetWindow_Click(object? sender, EventArgs e)
@ -412,12 +630,30 @@ public partial class GraphForm : Form
WindowState = FormWindowState.Normal; 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) private void MenuMiscCaches_Click(object? sender, EventArgs e)
{ {
if (this.cacheForm is not null)
{
this.cacheForm.Focus();
return;
}
ViewCacheForm cacheForm = new(this) ViewCacheForm cacheForm = new(this)
{ {
StartPosition = FormStartPosition.Manual StartPosition = FormStartPosition.Manual
}; };
this.cacheForm = cacheForm;
cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10, cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2); Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2);
@ -429,7 +665,7 @@ public partial class GraphForm : Form
cacheForm.TopMost = true; cacheForm.TopMost = true;
cacheForm.Show(); cacheForm.Show();
} }
private void MiscMenuPreload_Click(object sender, EventArgs e) private void MiscMenuPreload_Click(object? sender, EventArgs e)
{ {
Float2 min = MinVisibleGraph, max = MaxVisibleGraph; Float2 min = MinVisibleGraph, max = MaxVisibleGraph;
Float2 add = new(max.x - min.x, max.y - min.y); 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); foreach (Graphable able in Graphables) able.Preload(xRange, yRange, step);
Invalidate(false); 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; namespace Graphing.Forms
using System.Windows.Forms;
namespace Graphing.Forms
{ {
partial class SetZoomForm partial class SetZoomForm
{ {
@ -31,90 +28,157 @@ namespace Graphing.Forms
/// </summary> /// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
MessageLabel = new Label(); EnableBoxSelect = new System.Windows.Forms.Button();
ZoomTrackBar = new TrackBar(); MatchAspectButton = new System.Windows.Forms.Button();
ValueLabel = new Label(); ResetButton = new System.Windows.Forms.Button();
ZoomMinValue = new TextBox(); NormalizeButton = new System.Windows.Forms.Button();
ZoomMaxValue = new TextBox(); MinBoxX = new System.Windows.Forms.TextBox();
((System.ComponentModel.ISupportInitialize)ZoomTrackBar).BeginInit(); 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(); SuspendLayout();
// //
// MessageLabel // EnableBoxSelect
// //
MessageLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; EnableBoxSelect.Location = new System.Drawing.Point(12, 12);
MessageLabel.Location = new Point(52, 20); EnableBoxSelect.Name = "EnableBoxSelect";
MessageLabel.Name = "MessageLabel"; EnableBoxSelect.Size = new System.Drawing.Size(187, 46);
MessageLabel.Size = new Size(413, 35); EnableBoxSelect.TabIndex = 0;
MessageLabel.TabIndex = 0; EnableBoxSelect.Text = "Box Select";
MessageLabel.Text = "Set the zoom level for the graph."; EnableBoxSelect.UseVisualStyleBackColor = true;
MessageLabel.TextAlign = ContentAlignment.MiddleCenter; EnableBoxSelect.Click += EnableBoxSelect_Click;
// //
// ZoomTrackBar // MatchAspectButton
// //
ZoomTrackBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; MatchAspectButton.Location = new System.Drawing.Point(12, 64);
ZoomTrackBar.LargeChange = 1000; MatchAspectButton.Name = "MatchAspectButton";
ZoomTrackBar.Location = new Point(12, 127); MatchAspectButton.Size = new System.Drawing.Size(187, 46);
ZoomTrackBar.Maximum = 10000; MatchAspectButton.TabIndex = 1;
ZoomTrackBar.Name = "ZoomTrackBar"; MatchAspectButton.Text = "Match Aspect";
ZoomTrackBar.Size = new Size(489, 90); MatchAspectButton.UseVisualStyleBackColor = true;
ZoomTrackBar.TabIndex = 1; MatchAspectButton.Click += MatchAspectButton_Click;
ZoomTrackBar.TickStyle = TickStyle.None;
ZoomTrackBar.Scroll += ZoomTrackBar_Scroll;
// //
// ValueLabel // ResetButton
// //
ValueLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; ResetButton.Location = new System.Drawing.Point(12, 168);
ValueLabel.Location = new Point(52, 91); ResetButton.Name = "ResetButton";
ValueLabel.Name = "ValueLabel"; ResetButton.Size = new System.Drawing.Size(187, 46);
ValueLabel.Size = new Size(413, 33); ResetButton.TabIndex = 2;
ValueLabel.TabIndex = 2; ResetButton.Text = "Reset";
ValueLabel.Text = "1.00x"; ResetButton.UseVisualStyleBackColor = true;
ValueLabel.TextAlign = ContentAlignment.TopCenter; ResetButton.Click += ResetButton_Click;
// //
// ZoomMinValue // NormalizeButton
// //
ZoomMinValue.Location = new Point(12, 178); NormalizeButton.Location = new System.Drawing.Point(12, 116);
ZoomMinValue.Name = "ZoomMinValue"; NormalizeButton.Name = "NormalizeButton";
ZoomMinValue.Size = new Size(83, 39); NormalizeButton.Size = new System.Drawing.Size(187, 46);
ZoomMinValue.TabIndex = 3; NormalizeButton.TabIndex = 3;
ZoomMinValue.Text = "0.50"; NormalizeButton.Text = "Normalize";
ZoomMinValue.TextChanged += ZoomMinValue_TextChanged; NormalizeButton.UseVisualStyleBackColor = true;
NormalizeButton.Click += NormalizeButton_Click;
// //
// ZoomMaxValue // MinBoxX
// //
ZoomMaxValue.Anchor = AnchorStyles.Top | AnchorStyles.Right; MinBoxX.Location = new System.Drawing.Point(227, 49);
ZoomMaxValue.Location = new Point(418, 178); MinBoxX.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3);
ZoomMaxValue.Name = "ZoomMaxValue"; MinBoxX.Name = "MinBoxX";
ZoomMaxValue.Size = new Size(83, 39); MinBoxX.Size = new System.Drawing.Size(108, 39);
ZoomMaxValue.TabIndex = 4; MinBoxX.TabIndex = 4;
ZoomMaxValue.Text = "2.00"; //
ZoomMaxValue.TextAlign = HorizontalAlignment.Right; // TextX
ZoomMaxValue.TextChanged += ZoomMaxValue_TextChanged; //
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 // SetZoomForm
// //
AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new Size(513, 230); ClientSize = new System.Drawing.Size(533, 227);
Controls.Add(ZoomMaxValue); Controls.Add(ViewportLock);
Controls.Add(ZoomMinValue); Controls.Add(MaxBoxY);
Controls.Add(ValueLabel); Controls.Add(TextY);
Controls.Add(ZoomTrackBar); Controls.Add(MinBoxY);
Controls.Add(MessageLabel); Controls.Add(MaxBoxX);
FormBorderStyle = FormBorderStyle.FixedToolWindow; 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"; Name = "SetZoomForm";
Text = "Zoom Level"; Text = "Set Viewport Zoom";
((System.ComponentModel.ISupportInitialize)ZoomTrackBar).EndInit();
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
} }
#endregion #endregion
private Label MessageLabel; private System.Windows.Forms.Button EnableBoxSelect;
private TrackBar ZoomTrackBar; private System.Windows.Forms.Button MatchAspectButton;
private Label ValueLabel; private System.Windows.Forms.Button ResetButton;
private TextBox ZoomMinValue; private System.Windows.Forms.Button NormalizeButton;
private TextBox ZoomMaxValue; 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 public partial class SetZoomForm : Form
{ {
private double minZoomRange; private readonly GraphForm refForm;
private double maxZoomRange;
private double zoomLevel; private bool boxSelectEnabled;
private readonly GraphForm form; public SetZoomForm(GraphForm refForm)
public SetZoomForm(GraphForm form)
{ {
InitializeComponent(); InitializeComponent();
this.refForm = refForm;
minZoomRange = 1 / (form.ZoomLevel * 2); refForm.Paint += (o, e) => RedeclareValues();
maxZoomRange = 2 / form.ZoomLevel; RedeclareValues();
zoomLevel = 1 / form.ZoomLevel;
ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum); MinBoxX.Leave += MinBoxX_Finish;
MinBoxX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MinBoxX_Finish(o, e);
};
MaxBoxX.Leave += MaxBoxX_Finish;
MaxBoxX.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MaxBoxX_Finish(o, e);
};
this.form = form; MinBoxY.Leave += MinBoxY_Finish;
MinBoxY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MinBoxY_Finish(o, e);
};
MaxBoxY.Leave += MaxBoxY_Finish;
MaxBoxY.KeyDown += (o, e) =>
{
if (e.KeyCode == Keys.Enter) MaxBoxY_Finish(o, e);
};
} }
protected override void OnPaint(PaintEventArgs e) private void EnableBoxSelect_Click(object? sender, EventArgs e)
{ {
ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); boxSelectEnabled = !boxSelectEnabled;
ZoomMinValue.Text = minZoomRange.ToString("0.00"); refForm.canBoxSelect = boxSelectEnabled;
ValueLabel.Text = $"{zoomLevel:0.00}x"; if (boxSelectEnabled)
base.OnPaint(e);
form.ZoomLevel = 1 / zoomLevel;
form.Invalidate(false);
}
private double FactorToZoom(double factor)
{ {
return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange); EnableBoxSelect.Text = $"Cancel ...";
} refForm.Focus();
private double ZoomToFactor(double zoom)
{
double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange);
return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue));
}
private void ZoomTrackBar_Scroll(object? sender, EventArgs e)
{
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
zoomLevel = FactorToZoom(factor);
Invalidate(true);
}
private void ZoomMinValue_TextChanged(object? sender, EventArgs e)
{
double original = minZoomRange;
try
{
double value;
if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) ||
ZoomMinValue.Text.EndsWith('.'))
{
return;
} }
else else
{ {
value = double.Parse(ZoomMinValue.Text); EnableBoxSelect.Text = "Box Select";
if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new(); }
}
private void MatchAspectButton_Click(object? sender, EventArgs e)
{
double zoomXFactor = refForm.ZoomLevel.x / refForm.ZoomLevel.y;
double actualXFactor = refForm.ClientRectangle.Width / refForm.ClientRectangle.Height;
double diff = actualXFactor / zoomXFactor;
int newWidth = (int)(refForm.Width / diff);
refForm.ZoomLevel = new(refForm.ZoomLevel.x * diff, refForm.ZoomLevel.y);
int maxScreenWidth = Screen.FromControl(refForm).WorkingArea.Width;
if (newWidth >= maxScreenWidth)
{
refForm.Location = new(0, refForm.Location.Y);
double xScaleFactor = (double)maxScreenWidth / newWidth;
newWidth = maxScreenWidth;
refForm.Height = (int)(refForm.Height * xScaleFactor);
refForm.ZoomLevel = new(refForm.ZoomLevel.x * xScaleFactor, refForm.ZoomLevel.y * xScaleFactor);
} }
minZoomRange = value; refForm.Width = newWidth;
ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
double newZoom = FactorToZoom(factor);
zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
} }
catch private void NormalizeButton_Click(object? sender, EventArgs e)
{ {
minZoomRange = original; double factor = 1 / Math.Min(refForm.ZoomLevel.x, refForm.ZoomLevel.y);
ZoomMinValue.Text = minZoomRange.ToString("0.00"); refForm.ZoomLevel = new(factor * refForm.ZoomLevel.x, factor * refForm.ZoomLevel.y);
} }
private void ResetButton_Click(object? sender, EventArgs e)
{
refForm.ResetAllViewport();
}
private void ViewportLock_CheckedChanged(object? sender, EventArgs e)
{
refForm.ViewportLocked = ViewportLock.Checked;
RedeclareValues();
} }
private void ZoomMaxValue_TextChanged(object sender, EventArgs e) private void MinBoxX_Finish(object? sender, EventArgs e)
{ {
double original = maxZoomRange; if (double.TryParse(MinBoxX.Text, out double minX))
try
{ {
double value; Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) ||
ZoomMaxValue.Text.EndsWith('.')) if (minX > max.x)
{ {
return; MaxBoxX.Text = MinBoxX.Text;
} MaxBoxX_Finish(sender, e);
else minX = max.x;
{
value = double.Parse(ZoomMaxValue.Text); // Redefine bounds.
if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new(); min = refForm.MinVisibleGraph;
max = refForm.MaxVisibleGraph;
} }
maxZoomRange = value; double newCenterX = (minX + max.x) / 2,
ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); zoomFactorX = (max.x - minX) / (max.x - min.x);
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
double newZoom = FactorToZoom(factor);
zoomLevel = newZoom; refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y);
if (newZoom != factor) Invalidate(true); refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y);
} }
catch
refForm.Invalidate(false);
}
private void MaxBoxX_Finish(object? sender, EventArgs e)
{ {
maxZoomRange = original; if (double.TryParse(MaxBoxX.Text, out double maxX))
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) foreach (Graphable able in refForm.Graphables)
{ {
long thisBytes = able.GetCacheBytes(); long thisBytes = able.GetCacheBytes();
if (thisBytes == 0) continue;
CachePie.Values.Add((able.Color, thisBytes)); CachePie.Values.Add((able.Color, thisBytes));
totalBytes += thisBytes; totalBytes += thisBytes;

View File

@ -30,12 +30,12 @@ public abstract class Graphable
public abstract IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph); public abstract IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph);
public abstract Graphable DeepCopy(); public abstract Graphable ShallowCopy();
public virtual void EraseCache() { } public virtual void EraseCache() { }
public virtual long GetCacheBytes() => 0; public virtual long GetCacheBytes() => 0;
public virtual void Preload(Float2 xRange, Float2 yRange, double step) { } public virtual void Preload(Float2 xRange, Float2 yRange, double step) { }
public virtual bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor) => false; 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 Graphing.Parts;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Linq; using System.Linq;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -23,6 +24,7 @@ public class ColumnTable : Graphable
} }
public ColumnTable(double step, Equation equation, double min, double max) public ColumnTable(double step, Equation equation, double min, double max)
{ {
Color = equation.Color;
Name = $"Column Table for {equation.Name}"; Name = $"Column Table for {equation.Name}";
tableXY = []; tableXY = [];
@ -37,7 +39,7 @@ public class ColumnTable : Graphable
public override long GetCacheBytes() => 16 * tableXY.Count; 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) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
@ -45,12 +47,87 @@ public class ColumnTable : Graphable
foreach (KeyValuePair<double, double> col in tableXY) foreach (KeyValuePair<double, double> col in tableXY)
{ {
items.Add(GraphRectangle.FromSize(new Float2(col.Key, col.Value / 2), 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; 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. // Nothing to preload, everything is already cached.
public override void Preload(Float2 xRange, Float2 yRange, double step) { } public override void Preload(Float2 xRange, Float2 yRange, double step) { }
} }

View File

@ -3,16 +3,26 @@ using Graphing.Forms;
using Graphing.Parts; using Graphing.Parts;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables; namespace Graphing.Graphables;
public class Equation : Graphable, IIntegrable, IDerivable public class Equation : Graphable, IIntegrable, IDerivable, ITranslatableXY, IConvertSlopeField,
IConvertColumnTable
{ {
private static int equationNum; 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 EquationDelegate equ;
protected readonly List<Float2> cache; protected readonly List<Float2> cache;
public event Action<GraphForm> OnInvalidate;
public Equation(EquationDelegate equ) public Equation(EquationDelegate equ)
{ {
equationNum++; equationNum++;
@ -20,6 +30,11 @@ public class Equation : Graphable, IIntegrable, IDerivable
this.equ = equ; this.equ = equ;
cache = []; cache = [];
OffsetX = 0;
OffsetY = 0;
OnInvalidate = delegate { };
} }
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
@ -46,31 +61,44 @@ public class Equation : Graphable, IIntegrable, IDerivable
previousX = currentX; previousX = currentX;
previousY = currentY; previousY = currentY;
} }
OnInvalidate.Invoke(graph);
return lines; return lines;
} }
public Graphable Derive() => new Equation(x => protected double DerivativeAtPoint(double x)
{ {
const double step = 1e-3; 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 Graphable Integrate() => new IntegralEquation(this);
public EquationDelegate GetDelegate() => equ; 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(); public override void EraseCache() => cache.Clear();
protected double GetFromCache(double x, double epsilon) protected double GetFromCache(double x, double epsilon)
{ {
(double dist, double nearest, int index) = NearestCachedPoint(x); (double dist, double nearest, int index) = NearestCachedPoint(x - OffsetX);
if (dist < epsilon) return nearest; if (dist < epsilon) return nearest + OffsetY;
else else
{ {
double result = equ(x); double result = equ(x - OffsetX);
cache.Insert(index + 1, new(x, result)); cache.Insert(index + 1, new(x - OffsetX, result));
return result; return result + OffsetY;
} }
} }
public double GetValueAt(double x) => GetFromCache(x, 0);
protected (double dist, double y, int index) NearestCachedPoint(double x) protected (double dist, double y, int index) NearestCachedPoint(double x)
{ {
if (cache.Count == 0) return (double.PositiveInfinity, double.NaN, -1); 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; 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); double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist; return totalDist <= allowedDist;
} }
public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => public override IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3)); {
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) 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; usingAlt = true;
} }
public override Graphable DeepCopy() => new IntegralEquation(this); public override Graphable ShallowCopy() => new IntegralEquation(this);
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
@ -164,7 +164,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
} }
else 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() public Graphable Derive()
{ {
if (usingAlt) return altBaseEqu!.DeepCopy(); if (usingAlt) return altBaseEqu!.ShallowCopy();
else return (Equation)baseEqu!.DeepCopy(); else return (Equation)baseEqu!.ShallowCopy();
} }
public Graphable Integrate() => new IntegralEquation(this); 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); double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist; return totalDist <= allowedDist;
} }
public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => public override IEnumerable<IGraphPart> GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) =>
new(graphMousePos.x, IntegralAtPoint(graphMousePos.x)); [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 Graphing.Parts;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -9,29 +10,50 @@ public class SlopeField : Graphable
{ {
private static int slopeFieldNum; private static int slopeFieldNum;
protected readonly SlopeFieldsDelegate equ; public double Detail
protected readonly int 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; protected readonly List<(Float2, GraphLine)> cache;
public SlopeField(int detail, SlopeFieldsDelegate equ) public SlopeField(double detail, SlopeFieldsDelegate equ)
{ {
slopeFieldNum++; slopeFieldNum++;
Name = $"Slope Field {slopeFieldNum}"; Name = $"Slope Field {slopeFieldNum}";
this.equ = equ; this.equ = equ;
this.detail = detail; _detail = detail;
cache = []; cache = [];
} }
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph) 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 = []; 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)); lines.Add(GetFromCache(epsilon, x, y));
} }
@ -42,7 +64,7 @@ public class SlopeField : Graphable
protected GraphLine MakeSlopeLine(Float2 position, double slope) protected GraphLine MakeSlopeLine(Float2 position, double slope)
{ {
double size = detail; double size = _detail;
double dirX = size, dirY = slope * size; double dirX = size, dirY = slope * size;
double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY); double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY);
@ -72,17 +94,17 @@ public class SlopeField : Graphable
return result; 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 void EraseCache() => cache.Clear();
public override long GetCacheBytes() => cache.Count * 48; public override long GetCacheBytes() => cache.Count * 48;
public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor) public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
{ {
Float2 nearestPos = new(Math.Round(graphMousePos.x * detail) / detail, Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail,
Math.Round(graphMousePos.y * 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); GraphLine line = GetFromCache(epsilon, nearestPos.x, nearestPos.y);
double slope = (line.b.y - line.a.y) / (line.b.x - line.a.x); 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); double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist; 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, Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail,
Math.Round(graphMousePos.y * 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); GraphLine line = GetFromCache(epsilon, nearestPos.x, nearestPos.y);
double slope = (line.b.y - line.a.y) / (line.b.x - line.a.x); 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; lineY = slope * (lineX - nearestPos.x) + nearestPos.y;
Float2 point = new(lineX, lineY); 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) 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); GetFromCache(step, x, y);
} }

View File

@ -1,12 +1,16 @@
using Graphing.Forms; using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts; using Graphing.Parts;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
namespace Graphing.Graphables; namespace Graphing.Graphables;
public class TangentLine : Graphable public class TangentLine : Graphable, IConvertEquation, ITranslatableX
{ {
public bool UngraphWhenConvertedToEquation => true;
public double Position public double Position
{ {
get => _position; get => _position;
@ -18,8 +22,13 @@ public class TangentLine : Graphable
} }
private double _position; // Private because it has exactly the same functionality as `Position`. 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 Equation parent;
protected readonly EquationDelegate parentEqu;
protected readonly double length; protected readonly double length;
@ -35,16 +44,26 @@ public class TangentLine : Graphable
Name = $"Tangent Line of {parent.Name}"; Name = $"Tangent Line of {parent.Name}";
slopeCache = []; slopeCache = [];
parentEqu = parent.GetDelegate();
Position = position;
this.length = length; this.length = length;
this.parent = parent; 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) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
Float2 point = new(Position, currentSlope.y); Float2 point = new(Position, currentSlope.y);
return [MakeSlopeLine(), new GraphUiCircle(point, 8)]; return
[
MakeSlopeLine(),
new GraphUiCircle(point)
];
} }
protected GraphLine MakeSlopeLine() protected GraphLine MakeSlopeLine()
{ {
@ -63,13 +82,13 @@ public class TangentLine : Graphable
const double step = 1e-3; const double step = 1e-3;
double initial = parentEqu(x); double initial = parent.GetValueAt(x);
Float2 result = new((parentEqu(x + step) - initial) / step, initial); Float2 result = new((parent.GetValueAt(x + step) - initial) / step, initial);
slopeCache.Add(x, result); slopeCache.Add(x, result);
return 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 void EraseCache() => slopeCache.Clear();
public override long GetCacheBytes() => slopeCache.Count * 24; 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); double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist; 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(); GraphLine line = MakeSlopeLine();
@ -101,7 +120,15 @@ public class TangentLine : Graphable
Math.Min(line.a.x, line.b.x), Math.Min(line.a.x, line.b.x),
Math.Max(line.a.x, line.b.x)), Math.Max(line.a.x, line.b.x)),
lineY = currentSlope.x * (lineX - Position) + currentSlope.y; 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) public override void Preload(Float2 xRange, Float2 yRange, double step)
@ -112,4 +139,14 @@ public class TangentLine : Graphable
// that can be changed. // that can be changed.
for (double x = xRange.x; x <= xRange.y; x += step) DerivativeAtPoint(x); 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 record struct GraphRectangle : IGraphPart
{ {
public Float2 min, max; public Float2 min, max;
public double opacity;
public GraphRectangle() public GraphRectangle()
{ {
min = new(); min = new();
max = 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, min = new(center.x - size.x / 2,
center.y - size.y / 2), center.y - size.y / 2),
max = new(center.x + size.x / 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, min = min,
max = max max = max,
opacity = opacity,
}; };
public void Render(in GraphForm form, in Graphics g, in Pen pen) 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); start.y - end.y);
if (size.x == 0 || size.y == 0) return; 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)); 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(); center = new();
radius = 1; radius = 1;
} }
public GraphUiCircle(Float2 center, int radius) public GraphUiCircle(Float2 center, int radius = 8)
{ {
this.center = center; this.center = center;
this.radius = radius; 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> <Project>
<PropertyGroup> <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 /> <LastFailureDetails />
</PropertyGroup> </PropertyGroup>
</Project> </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. 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: 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. - There are currently some rendering issues with asymptotes which will be focused on at some point.
- Graph parametric equations.
- Integrate and derive equations. - Integrate and derive equations.
- Graph a slope field of a `dy/dx =` style equation. - Graph a slope field of a `dy/dx =` style equation.
- View a tangent line of an equation. - View a tangent line of an equation.

View File

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