diff --git a/Base/Abstract/IConvertColumnTable.cs b/Base/Abstract/IConvertColumnTable.cs new file mode 100644 index 0000000..4b87aaf --- /dev/null +++ b/Base/Abstract/IConvertColumnTable.cs @@ -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); +} diff --git a/Base/Abstract/IConvertEquation.cs b/Base/Abstract/IConvertEquation.cs new file mode 100644 index 0000000..da4f11a --- /dev/null +++ b/Base/Abstract/IConvertEquation.cs @@ -0,0 +1,10 @@ +using Graphing.Graphables; + +namespace Graphing.Abstract; + +public interface IConvertEquation +{ + public bool UngraphWhenConvertedToEquation { get; } + + public Equation ToEquation(); +} diff --git a/Base/Abstract/IConvertSlopeField.cs b/Base/Abstract/IConvertSlopeField.cs new file mode 100644 index 0000000..2c3982a --- /dev/null +++ b/Base/Abstract/IConvertSlopeField.cs @@ -0,0 +1,10 @@ +using Graphing.Graphables; + +namespace Graphing.Abstract; + +public interface IConvertSlopeField +{ + public bool UngraphWhenConvertedToSlopeField { get; } + + public SlopeField ToSlopeField(int detail); +} diff --git a/Base/Abstract/ITranslatable.cs b/Base/Abstract/ITranslatable.cs new file mode 100644 index 0000000..0f9e8a5 --- /dev/null +++ b/Base/Abstract/ITranslatable.cs @@ -0,0 +1,3 @@ +namespace Graphing.Abstract; + +public interface ITranslatable { } diff --git a/Base/Abstract/ITranslatableX.cs b/Base/Abstract/ITranslatableX.cs new file mode 100644 index 0000000..ed13456 --- /dev/null +++ b/Base/Abstract/ITranslatableX.cs @@ -0,0 +1,6 @@ +namespace Graphing.Abstract; + +public interface ITranslatableX : ITranslatable +{ + public double OffsetX { get; set; } +} diff --git a/Base/Abstract/ITranslatableXY.cs b/Base/Abstract/ITranslatableXY.cs new file mode 100644 index 0000000..6414127 --- /dev/null +++ b/Base/Abstract/ITranslatableXY.cs @@ -0,0 +1,3 @@ +namespace Graphing.Abstract; + +public interface ITranslatableXY : ITranslatableX, ITranslatableY { } diff --git a/Base/Abstract/ITranslatableY.cs b/Base/Abstract/ITranslatableY.cs new file mode 100644 index 0000000..f7ad103 --- /dev/null +++ b/Base/Abstract/ITranslatableY.cs @@ -0,0 +1,6 @@ +namespace Graphing.Abstract; + +public interface ITranslatableY : ITranslatable +{ + public double OffsetY { get; set; } +} diff --git a/Base/Base.csproj b/Base/Base.csproj index 2301302..54d38db 100644 --- a/Base/Base.csproj +++ b/Base/Base.csproj @@ -12,18 +12,18 @@ True ThatOneNerd.Graphing ThatOneNerd.Graphing - 1.2.0 + 1.3.0 That_One_Nerd A fairly adept graphing calculator made in Windows Forms. MIT https://github.com/That-One-Nerd/Graphing README.md - graphing;graph;plot;math;calculus;visual;desmos;slope field;slopefield;equation;visualizer + graphing;graph;plot;math;calculus;visual;desmos;slope field;slopefield;equation;visualizer;parametric equation;parametric;difference;tangent MIT True snupkg View the GitHub release for the changelog: -https://github.com/That-One-Nerd/Graphing/releases/tag/1.2.0 +https://github.com/That-One-Nerd/Graphing/releases/tag/1.3.0 diff --git a/Base/Base.csproj.user b/Base/Base.csproj.user index ef577eb..8d9b1c8 100644 --- a/Base/Base.csproj.user +++ b/Base/Base.csproj.user @@ -16,6 +16,12 @@ Form + + Form + + + Form + Form diff --git a/Base/Forms/GraphForm.Designer.cs b/Base/Forms/GraphForm.Designer.cs index f30584b..ec64887 100644 --- a/Base/Forms/GraphForm.Designer.cs +++ b/Base/Forms/GraphForm.Designer.cs @@ -38,33 +38,45 @@ namespace Graphing.Forms ButtonViewportSetCenter = new ToolStripMenuItem(); ButtonViewportReset = new ToolStripMenuItem(); ButtonViewportResetWindow = new ToolStripMenuItem(); - MenuColors = new ToolStripMenuItem(); - MenuEquations = new ToolStripMenuItem(); - MenuEquationsDerivative = new ToolStripMenuItem(); - MenuEquationsIntegral = new ToolStripMenuItem(); + MenuElements = new ToolStripMenuItem(); + MenuElementsColors = new ToolStripMenuItem(); + MenuElementsRemove = new ToolStripMenuItem(); + MenuOperations = new ToolStripMenuItem(); + MenuOperationsDerivative = new ToolStripMenuItem(); + MenuOperationsIntegral = new ToolStripMenuItem(); + MenuOperationsTranslate = new ToolStripMenuItem(); + MenuConvert = new ToolStripMenuItem(); + MenuConvertEquation = new ToolStripMenuItem(); + MenuConvertSlopeField = new ToolStripMenuItem(); MenuMisc = new ToolStripMenuItem(); MenuMiscCaches = new ToolStripMenuItem(); MiscMenuPreload = new ToolStripMenuItem(); + UpdaterPopup = new Panel(); + UpdaterPopupDownloadButton = new Button(); + UpdaterPopupCloseButton = new Button(); + UpdaterPopupMessage = new Label(); + MenuElementsDetail = new ToolStripMenuItem(); GraphMenu.SuspendLayout(); + UpdaterPopup.SuspendLayout(); SuspendLayout(); // // ResetViewportButton // ResetViewportButton.Anchor = AnchorStyles.Top | AnchorStyles.Right; - ResetViewportButton.Font = new Font("Segoe UI Emoji", 13.875F, FontStyle.Regular, GraphicsUnit.Point, 0); - ResetViewportButton.Location = new Point(1373, 43); + ResetViewportButton.Font = new Font("Segoe UI Emoji", 12F, FontStyle.Regular, GraphicsUnit.Point, 0); + ResetViewportButton.Location = new Point(1372, 43); + ResetViewportButton.Margin = new Padding(4, 2, 4, 2); ResetViewportButton.Name = "ResetViewportButton"; - ResetViewportButton.Size = new Size(64, 64); + ResetViewportButton.Size = new Size(63, 64); ResetViewportButton.TabIndex = 0; - ResetViewportButton.Text = "⌂"; - ResetViewportButton.TextAlign = ContentAlignment.TopRight; + ResetViewportButton.Text = "🏠"; ResetViewportButton.UseVisualStyleBackColor = true; ResetViewportButton.Click += ResetViewportButton_Click; // // GraphMenu // GraphMenu.ImageScalingSize = new Size(32, 32); - GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuColors, MenuEquations, MenuMisc }); + GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuElements, MenuOperations, MenuConvert, MenuMisc }); GraphMenu.Location = new Point(0, 0); GraphMenu.Name = "GraphMenu"; GraphMenu.Size = new Size(1449, 42); @@ -81,55 +93,93 @@ namespace Graphing.Forms // ButtonViewportSetZoom // ButtonViewportSetZoom.Name = "ButtonViewportSetZoom"; - ButtonViewportSetZoom.Size = new Size(350, 44); + ButtonViewportSetZoom.Size = new Size(359, 44); ButtonViewportSetZoom.Text = "Set Zoom"; ButtonViewportSetZoom.Click += ButtonViewportSetZoom_Click; // // ButtonViewportSetCenter // ButtonViewportSetCenter.Name = "ButtonViewportSetCenter"; - ButtonViewportSetCenter.Size = new Size(350, 44); + ButtonViewportSetCenter.Size = new Size(359, 44); ButtonViewportSetCenter.Text = "Set Center Position"; ButtonViewportSetCenter.Click += ButtonViewportSetCenter_Click; // // ButtonViewportReset // ButtonViewportReset.Name = "ButtonViewportReset"; - ButtonViewportReset.Size = new Size(350, 44); + ButtonViewportReset.Size = new Size(359, 44); ButtonViewportReset.Text = "Reset Viewport"; ButtonViewportReset.Click += ButtonViewportReset_Click; // // ButtonViewportResetWindow // ButtonViewportResetWindow.Name = "ButtonViewportResetWindow"; - ButtonViewportResetWindow.Size = new Size(350, 44); + ButtonViewportResetWindow.Size = new Size(359, 44); ButtonViewportResetWindow.Text = "Reset Window Size"; ButtonViewportResetWindow.Click += ButtonViewportResetWindow_Click; // - // MenuColors + // MenuElements // - MenuColors.Name = "MenuColors"; - MenuColors.Size = new Size(101, 38); - MenuColors.Text = "Colors"; + MenuElements.DropDownItems.AddRange(new ToolStripItem[] { MenuElementsColors, MenuElementsDetail, MenuElementsRemove }); + MenuElements.Name = "MenuElements"; + MenuElements.Size = new Size(131, 38); + MenuElements.Text = "Elements"; // - // MenuEquations + // MenuElementsColors // - MenuEquations.DropDownItems.AddRange(new ToolStripItem[] { MenuEquationsDerivative, MenuEquationsIntegral }); - MenuEquations.Name = "MenuEquations"; - MenuEquations.Size = new Size(138, 38); - MenuEquations.Text = "Equations"; + MenuElementsColors.Name = "MenuElementsColors"; + MenuElementsColors.Size = new Size(359, 44); + MenuElementsColors.Text = "Colors"; // - // MenuEquationsDerivative + // MenuElementsRemove // - MenuEquationsDerivative.Name = "MenuEquationsDerivative"; - MenuEquationsDerivative.Size = new Size(360, 44); - MenuEquationsDerivative.Text = "Compute Derivative"; + MenuElementsRemove.Name = "MenuElementsRemove"; + MenuElementsRemove.Size = new Size(359, 44); + MenuElementsRemove.Text = "Remove"; // - // MenuEquationsIntegral + // MenuOperations // - MenuEquationsIntegral.Name = "MenuEquationsIntegral"; - MenuEquationsIntegral.Size = new Size(360, 44); - MenuEquationsIntegral.Text = "Compute Integral"; + MenuOperations.DropDownItems.AddRange(new ToolStripItem[] { MenuOperationsDerivative, MenuOperationsIntegral, MenuOperationsTranslate }); + MenuOperations.Name = "MenuOperations"; + MenuOperations.Size = new Size(151, 38); + MenuOperations.Text = "Operations"; + // + // MenuOperationsDerivative + // + MenuOperationsDerivative.Name = "MenuOperationsDerivative"; + MenuOperationsDerivative.Size = new Size(360, 44); + MenuOperationsDerivative.Text = "Compute Derivative"; + // + // MenuOperationsIntegral + // + MenuOperationsIntegral.Name = "MenuOperationsIntegral"; + MenuOperationsIntegral.Size = new Size(360, 44); + MenuOperationsIntegral.Text = "Compute Integral"; + // + // MenuOperationsTranslate + // + MenuOperationsTranslate.Name = "MenuOperationsTranslate"; + MenuOperationsTranslate.Size = new Size(360, 44); + MenuOperationsTranslate.Text = "Translate"; + // + // MenuConvert + // + MenuConvert.DropDownItems.AddRange(new ToolStripItem[] { MenuConvertEquation, MenuConvertSlopeField }); + MenuConvert.Name = "MenuConvert"; + MenuConvert.Size = new Size(118, 38); + MenuConvert.Text = "Convert"; + // + // MenuConvertEquation + // + MenuConvertEquation.Name = "MenuConvertEquation"; + MenuConvertEquation.Size = new Size(297, 44); + MenuConvertEquation.Text = "To Equation"; + // + // MenuConvertSlopeField + // + MenuConvertSlopeField.Name = "MenuConvertSlopeField"; + MenuConvertSlopeField.Size = new Size(297, 44); + MenuConvertSlopeField.Text = "To Slope Field"; // // MenuMisc // @@ -141,29 +191,87 @@ namespace Graphing.Forms // MenuMiscCaches // MenuMiscCaches.Name = "MenuMiscCaches"; - MenuMiscCaches.Size = new Size(359, 44); + MenuMiscCaches.Size = new Size(299, 44); MenuMiscCaches.Text = "View Caches"; MenuMiscCaches.Click += MenuMiscCaches_Click; // // MiscMenuPreload // MiscMenuPreload.Name = "MiscMenuPreload"; - MiscMenuPreload.Size = new Size(359, 44); + MiscMenuPreload.Size = new Size(299, 44); MiscMenuPreload.Text = "Preload Cache"; MiscMenuPreload.Click += MiscMenuPreload_Click; // + // UpdaterPopup + // + UpdaterPopup.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + UpdaterPopup.BackColor = SystemColors.HighlightText; + UpdaterPopup.BorderStyle = BorderStyle.FixedSingle; + UpdaterPopup.Controls.Add(UpdaterPopupDownloadButton); + UpdaterPopup.Controls.Add(UpdaterPopupCloseButton); + UpdaterPopup.Controls.Add(UpdaterPopupMessage); + UpdaterPopup.Location = new Point(966, 791); + UpdaterPopup.Margin = new Padding(6, 6, 6, 6); + UpdaterPopup.Name = "UpdaterPopup"; + UpdaterPopup.Size = new Size(483, 115); + UpdaterPopup.TabIndex = 2; + UpdaterPopup.Visible = false; + // + // UpdaterPopupDownloadButton + // + UpdaterPopupDownloadButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + UpdaterPopupDownloadButton.Location = new Point(336, 58); + UpdaterPopupDownloadButton.Margin = new Padding(6, 6, 6, 6); + UpdaterPopupDownloadButton.Name = "UpdaterPopupDownloadButton"; + UpdaterPopupDownloadButton.Size = new Size(139, 49); + UpdaterPopupDownloadButton.TabIndex = 2; + UpdaterPopupDownloadButton.Text = "Visit"; + UpdaterPopupDownloadButton.UseVisualStyleBackColor = true; + // + // UpdaterPopupCloseButton + // + UpdaterPopupCloseButton.Anchor = AnchorStyles.Top | AnchorStyles.Right; + UpdaterPopupCloseButton.Location = new Point(435, 2); + UpdaterPopupCloseButton.Margin = new Padding(2, 2, 2, 2); + UpdaterPopupCloseButton.Name = "UpdaterPopupCloseButton"; + UpdaterPopupCloseButton.Size = new Size(45, 51); + UpdaterPopupCloseButton.TabIndex = 1; + UpdaterPopupCloseButton.Text = "X"; + UpdaterPopupCloseButton.UseVisualStyleBackColor = true; + UpdaterPopupCloseButton.Click += UpdaterPopupCloseButton_Click; + // + // UpdaterPopupMessage + // + UpdaterPopupMessage.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; + UpdaterPopupMessage.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); + UpdaterPopupMessage.Location = new Point(6, 6); + UpdaterPopupMessage.Margin = new Padding(6, 6, 6, 6); + UpdaterPopupMessage.Name = "UpdaterPopupMessage"; + UpdaterPopupMessage.Size = new Size(423, 100); + UpdaterPopupMessage.TabIndex = 0; + UpdaterPopupMessage.Text = "A update is available!\r\nA.B.C → E.F.G"; + // + // MenuElementsDetail + // + MenuElementsDetail.Name = "MenuElementsDetail"; + MenuElementsDetail.Size = new Size(359, 44); + MenuElementsDetail.Text = "Detail"; + // // GraphForm // AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(1449, 907); + Controls.Add(UpdaterPopup); Controls.Add(ResetViewportButton); Controls.Add(GraphMenu); MainMenuStrip = GraphMenu; + Margin = new Padding(4, 2, 4, 2); Name = "GraphForm"; Text = "GraphFormBase"; GraphMenu.ResumeLayout(false); GraphMenu.PerformLayout(); + UpdaterPopup.ResumeLayout(false); ResumeLayout(false); PerformLayout(); } @@ -172,17 +280,28 @@ namespace Graphing.Forms private Button ResetViewportButton; private MenuStrip GraphMenu; - private ToolStripMenuItem MenuColors; private ToolStripMenuItem MenuViewport; private ToolStripMenuItem ButtonViewportSetZoom; private ToolStripMenuItem ButtonViewportSetCenter; private ToolStripMenuItem ButtonViewportReset; private ToolStripMenuItem ButtonViewportResetWindow; - private ToolStripMenuItem MenuEquations; - private ToolStripMenuItem MenuEquationsDerivative; - private ToolStripMenuItem MenuEquationsIntegral; + private ToolStripMenuItem MenuOperations; + private ToolStripMenuItem MenuOperationsDerivative; + private ToolStripMenuItem MenuOperationsIntegral; private ToolStripMenuItem MenuMisc; private ToolStripMenuItem MenuMiscCaches; private ToolStripMenuItem MiscMenuPreload; + private ToolStripMenuItem MenuConvert; + private ToolStripMenuItem MenuConvertEquation; + private ToolStripMenuItem MenuElements; + private ToolStripMenuItem MenuElementsColors; + private ToolStripMenuItem MenuElementsRemove; + private ToolStripMenuItem MenuOperationsTranslate; + private ToolStripMenuItem MenuConvertSlopeField; + private Panel UpdaterPopup; + private Label UpdaterPopupMessage; + private Button UpdaterPopupCloseButton; + private Button UpdaterPopupDownloadButton; + private ToolStripMenuItem MenuElementsDetail; } } \ No newline at end of file diff --git a/Base/Forms/GraphForm.cs b/Base/Forms/GraphForm.cs index 152d852..b4630ab 100644 --- a/Base/Forms/GraphForm.cs +++ b/Base/Forms/GraphForm.cs @@ -1,46 +1,71 @@ using Graphing.Abstract; -using Graphing.Parts; +using Graphing.Graphables; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Windows.Forms; namespace Graphing.Forms; public partial class GraphForm : Form { + public static readonly Color BackgroundColor = Color.White; public static readonly Color MainAxisColor = Color.Black; - public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); - public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0)); + public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); // Grayish + public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0)); // Lighter grayish public static readonly Color UnitsTextColor = Color.Black; + public static readonly Color ZoomBoxColor = Color.Black; - public Float2 ScreenCenter { get; private set; } + public static readonly Color MajorUpdateColor = Color.FromArgb(unchecked((int)0xFF_F74434)); // Red + public static readonly Color MinorUpdateColor = Color.FromArgb(unchecked((int)0xFF_FCA103)); // Orange + + public Float2 ScreenCenter { get; set; } public Float2 Dpi { get; private set; } public float DpiFloat { get; private set; } - public double ZoomLevel + public Float2 ZoomLevel { get => _zoomLevel; set { - double oldZoom = ZoomLevel; - - _zoomLevel = Math.Clamp(value, 1e-5, 1e3); - - int totalSegments = 0; - foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count(); - - if (totalSegments > 10_000) - { - _zoomLevel = oldZoom; - return; // Too many segments, stop. - } + _zoomLevel = new(Math.Clamp(value.x, 1e-5, 1e3), + Math.Clamp(value.y, 1e-5, 1e3)); + OnZoomLevelChanged(this, new()); + Invalidate(false); } } - private double _zoomLevel; + private Float2 _zoomLevel; + + public bool ViewportLocked + { + get => _viewportLocked; + set + { + if (value) + { + FormBorderStyle = FormBorderStyle.FixedSingle; + ResetViewportButton.Text = "🔒"; + } + else + { + FormBorderStyle = FormBorderStyle.Sizable; + ResetViewportButton.Text = "🏠"; + } + MaximizeBox = !value; + ResetViewportButton.Enabled = !value; + + _viewportLocked = value; + } + } + private bool _viewportLocked; private readonly Point initialWindowPos; private readonly Size initialWindowSize; @@ -52,6 +77,8 @@ public partial class GraphForm : Form private readonly List ables; + public event EventHandler OnZoomLevelChanged = delegate { }; + public GraphForm(string title) { SetStyle(ControlStyles.OptimizedDoubleBuffer, true); @@ -68,9 +95,11 @@ public partial class GraphForm : Form DpiFloat = (float)((Dpi.x + Dpi.y) / 2); ables = []; - ZoomLevel = 1; + ZoomLevel = new(1, 1); initialWindowPos = Location; initialWindowSize = Size; + + RunUpdateChecker(); } public Int2 GraphSpaceToScreenSpace(Float2 graphPoint) @@ -80,8 +109,8 @@ public partial class GraphForm : Form graphPoint.x -= ScreenCenter.x; graphPoint.y -= ScreenCenter.y; - graphPoint.x *= Dpi.x / ZoomLevel; - graphPoint.y *= Dpi.y / ZoomLevel; + graphPoint.x *= Dpi.x / ZoomLevel.x; + graphPoint.y *= Dpi.y / ZoomLevel.y; graphPoint.x += ClientRectangle.Width / 2.0; graphPoint.y += ClientRectangle.Height / 2.0; @@ -95,8 +124,8 @@ public partial class GraphForm : Form result.x -= ClientRectangle.Width / 2.0; result.y -= ClientRectangle.Height / 2.0; - result.x /= Dpi.x / ZoomLevel; - result.y /= Dpi.y / ZoomLevel; + result.x /= Dpi.x / ZoomLevel.x; + result.y /= Dpi.y / ZoomLevel.y; result.x += ScreenCenter.x; result.y += ScreenCenter.y; @@ -108,19 +137,20 @@ public partial class GraphForm : Form protected virtual void PaintGrid(Graphics g) { - double axisScale = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel))); + double axisScaleX = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.x))), + axisScaleY = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.y))); // Draw horizontal/vertical quarter-axis. Brush quarterBrush = new SolidBrush(QuarterAxisColor); Pen quarterPen = new(quarterBrush, DpiFloat * 2 / 192); - for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScale) * axisScale / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScale) * axisScale / 4; x += axisScale / 4) + for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScaleX) * axisScaleX / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScaleX) * axisScaleX / 4; x += axisScaleX / 4) { Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)), endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y)); g.DrawLine(quarterPen, startPos, endPos); } - for (double y = Math.Ceiling(MinVisibleGraph.y * 4 / axisScale) * axisScale / 4; y <= Math.Floor(MaxVisibleGraph.y * 4 / axisScale) * axisScale / 4; y += axisScale / 4) + for (double y = Math.Ceiling(MinVisibleGraph.y * 4 / axisScaleY) * axisScaleY / 4; y <= Math.Floor(MaxVisibleGraph.y * 4 / axisScaleY) * axisScaleY / 4; y += axisScaleY / 4) { Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)), endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y)); @@ -131,13 +161,13 @@ public partial class GraphForm : Form Brush semiBrush = new SolidBrush(SemiAxisColor); Pen semiPen = new(semiBrush, DpiFloat * 2 / 192); - for (double x = Math.Ceiling(MinVisibleGraph.x / axisScale) * axisScale; x <= Math.Floor(MaxVisibleGraph.x / axisScale) * axisScale; x += axisScale) + for (double x = Math.Ceiling(MinVisibleGraph.x / axisScaleX) * axisScaleX; x <= Math.Floor(MaxVisibleGraph.x / axisScaleX) * axisScaleX; x += axisScaleX) { Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)), endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y)); g.DrawLine(semiPen, startPos, endPos); } - for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= Math.Floor(MaxVisibleGraph.y / axisScale) * axisScale; y += axisScale) + for (double y = Math.Ceiling(MinVisibleGraph.y / axisScaleY) * axisScaleY; y <= Math.Floor(MaxVisibleGraph.y / axisScaleY) * axisScaleY; y += axisScaleY) { Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)), endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y)); @@ -158,14 +188,15 @@ public partial class GraphForm : Form } protected virtual void PaintUnits(Graphics g) { - double axisScale = Math.Pow(2, Math.Round(Math.Log(ZoomLevel, 2))); + double axisScaleX = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.x))), + axisScaleY = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.y))); Brush textBrush = new SolidBrush(UnitsTextColor); Font textFont = new(Font.Name, 9, FontStyle.Regular); // X-axis int minX = (int)(DpiFloat * 50 / 192), maxX = ClientRectangle.Height - (int)(DpiFloat * 40 / 192); - for (double x = Math.Ceiling(MinVisibleGraph.x / axisScale) * axisScale; x <= MaxVisibleGraph.x; x += axisScale) + for (double x = Math.Ceiling(MinVisibleGraph.x / axisScaleX) * axisScaleX; x <= MaxVisibleGraph.x; x += axisScaleX) { if (x == 0) x = 0; // Fixes -0 @@ -179,7 +210,7 @@ public partial class GraphForm : Form // Y-axis int minY = (int)(DpiFloat * 10 / 192); - for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= MaxVisibleGraph.y; y += axisScale) + for (double y = Math.Ceiling(MinVisibleGraph.y / axisScaleY) * axisScaleY; y <= MaxVisibleGraph.y; y += axisScaleY) { if (y == 0) continue; @@ -200,7 +231,7 @@ public partial class GraphForm : Form Graphics g = e.Graphics; g.SmoothingMode = SmoothingMode.HighQuality; - Brush background = new SolidBrush(Color.White); + Brush background = new SolidBrush(BackgroundColor); g.FillRectangle(background, e.ClipRectangle); PaintGrid(g); @@ -223,29 +254,31 @@ public partial class GraphForm : Form // Equation selection detection. // This system lets you select multiple graphs, and that's cool by me. - if (ableDrag) + if (selectState == SelectionState.GraphSelect) { - Font textFont = new(Font.Name, 8, FontStyle.Bold); for (int i = 0; i < ables.Count; i++) { if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5)) { - Float2 selectedPoint = ables[i].GetSelectedPoint(this, graphMousePos); - GraphUiCircle select = new(selectedPoint, 8); - - Int2 textPos = GraphSpaceToScreenSpace(select.center); - textPos.y -= (int)(DpiFloat * 32 / 192); - - string content = $"({selectedPoint.x:0.00}, {selectedPoint.y:0.00})"; - - SizeF textSize = g.MeasureString(content, textFont); - g.FillRectangle(background, new Rectangle(textPos.x, textPos.y, - (int)textSize.Width, (int)textSize.Height)); - g.DrawString(content, textFont, graphPens[i].Brush, new Point(textPos.x, textPos.y)); - select.Render(this, g, graphPens[i]); + IEnumerable selectionParts = ables[i].GetSelectionItemsToRender(this, graphMousePos); + foreach (IGraphPart selPart in selectionParts) selPart.Render(this, g, graphPens[i]); } } } + else if (selectState == SelectionState.ZoomBox) + { + // Draw the current box selection. + Int2 boxPosA = GraphSpaceToScreenSpace(boxSelectA), + boxPosB = GraphSpaceToScreenSpace(boxSelectB); + + if (boxPosA.x > boxPosB.x) (boxPosA.x, boxPosB.x) = (boxPosB.x, boxPosA.x); + if (boxPosA.y > boxPosB.y) (boxPosA.y, boxPosB.y) = (boxPosB.y, boxPosA.y); + + Pen boxPen = new(ZoomBoxColor, 2 * DpiFloat / 192); + g.DrawRectangle(boxPen, new(boxPosA.x, boxPosA.y, + boxPosB.x - boxPosA.x, + boxPosB.y - boxPosA.y)); + } base.OnPaint(e); } @@ -255,77 +288,152 @@ public partial class GraphForm : Form Invalidate(false); } - public void Graph(params Graphable[] able) + public void Graph(params Graphable[] newAbles) { - ables.AddRange(able); + ables.AddRange(newAbles); + RegenerateMenuItems(); + Invalidate(false); + } + public void Ungraph(params Graphable[] ables) + { + this.ables.RemoveAll(x => ables.Contains(x)); RegenerateMenuItems(); Invalidate(false); } - private bool mouseDrag = false; + public bool IsGraphPointVisible(Float2 point) + { + Int2 pixelPos = GraphSpaceToScreenSpace(point); + return pixelPos.x >= 0 && pixelPos.x < ClientRectangle.Width && + pixelPos.y >= 0 && pixelPos.y < ClientRectangle.Height; + } + + private SelectionState selectState = SelectionState.None; + internal bool canBoxSelect; + private SetZoomForm? setZoomForm; + private Int2 initialMouseLocation; private Float2 initialScreenCenter; - private bool ableDrag = false; + private Float2 boxSelectA, boxSelectB; + protected override void OnMouseDown(MouseEventArgs e) { - if (!mouseDrag) + if (selectState == SelectionState.None && canBoxSelect) + { + Point clientMousePos = PointToClient(Cursor.Position); + Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X, + clientMousePos.Y)); + + boxSelectA = graphMousePos; + boxSelectB = graphMousePos; + + selectState = SelectionState.ZoomBox; + } + + if (selectState == SelectionState.None) { Point clientMousePos = PointToClient(Cursor.Position); Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X, clientMousePos.Y)); foreach (Graphable able in Graphables) { - if (able.ShouldSelectGraphable(this, graphMousePos, 1)) ableDrag = true; + if (able.ShouldSelectGraphable(this, graphMousePos, 1)) + selectState = SelectionState.GraphSelect; } - if (ableDrag) Invalidate(false); + if (selectState == SelectionState.GraphSelect) Invalidate(false); } - if (!ableDrag) + if (selectState == SelectionState.None && !ViewportLocked) { - mouseDrag = true; + selectState = SelectionState.ViewportDrag; initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y); initialScreenCenter = ScreenCenter; } } protected override void OnMouseUp(MouseEventArgs e) { - if (mouseDrag) + if (selectState == SelectionState.None) return; + else if (selectState == SelectionState.ViewportDrag) { Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X, initialMouseLocation.y - Cursor.Position.Y); - Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y); + Float2 graphDiff = new(pixelDiff.x * ZoomLevel.x / Dpi.x, pixelDiff.y * ZoomLevel.y / Dpi.y); ScreenCenter = new(initialScreenCenter.x + graphDiff.x, initialScreenCenter.y + graphDiff.y); } - mouseDrag = false; - ableDrag = false; + else if (selectState == SelectionState.ZoomBox) + { + Point clientMousePos = PointToClient(Cursor.Position); + Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X, + clientMousePos.Y)); + boxSelectB = graphMousePos; + + // Set center. + ScreenCenter = new((boxSelectA.x + boxSelectB.x) * 0.5, + -(boxSelectA.y + boxSelectB.y) * 0.5); + + // Set zoom. Kind of weird but it works. + Float2 minGraph = MinVisibleGraph, maxGraph = MaxVisibleGraph; + Float2 oldDist = new(maxGraph.x - minGraph.x, + maxGraph.y - minGraph.y); + Float2 newDist = new(Math.Abs(boxSelectB.x - boxSelectA.x), + Math.Abs(boxSelectB.y - boxSelectA.y)); + ZoomLevel = new(ZoomLevel.x * newDist.x / oldDist.x, + ZoomLevel.y * newDist.y / oldDist.y); + + setZoomForm!.CompleteBoxSelection(); + + boxSelectA = new(0, 0); + boxSelectB = new(0, 0); + } + selectState = SelectionState.None; Invalidate(false); } protected override void OnMouseMove(MouseEventArgs e) { - if (mouseDrag) + if (selectState == SelectionState.None) return; + else if (selectState == SelectionState.ViewportDrag) { Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X, initialMouseLocation.y - Cursor.Position.Y); - Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y); + Float2 graphDiff = new(pixelDiff.x * ZoomLevel.x / Dpi.x, pixelDiff.y * ZoomLevel.y / Dpi.y); ScreenCenter = new(initialScreenCenter.x + graphDiff.x, initialScreenCenter.y + graphDiff.y); - Invalidate(false); } - else if (ableDrag) Invalidate(false); + else if (selectState == SelectionState.ZoomBox) + { + Point clientMousePos = PointToClient(Cursor.Position); + Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X, + clientMousePos.Y)); + boxSelectB = graphMousePos; + } + Invalidate(false); } protected override void OnMouseWheel(MouseEventArgs e) { - ZoomLevel *= 1 - e.Delta * 0.00075; // Zoom factor. + if (ViewportLocked) return; + + Point clientMousePos = PointToClient(Cursor.Position); + Int2 mousePos = new(clientMousePos.X, clientMousePos.Y); + Float2 mouseOver = ScreenSpaceToGraphSpace(mousePos); + + Float2 newZoom = ZoomLevel; + newZoom.x *= 1 - e.Delta * 0.00075; // Zoom factor. + newZoom.y *= 1 - e.Delta * 0.00075; + ZoomLevel = newZoom; + + // Keep the mouse as the zoom hotspot. + Float2 newOver = ScreenSpaceToGraphSpace(mousePos); + Float2 delta = new(newOver.x - mouseOver.x, newOver.y - mouseOver.y); + ScreenCenter = new(ScreenCenter.x - delta.x, ScreenCenter.y + delta.y); + Invalidate(false); } private void ResetViewportButton_Click(object? sender, EventArgs e) { - ScreenCenter = new Float2(0, 0); - ZoomLevel = 1; - Invalidate(false); + ResetAllViewport(); } private void GraphColorPickerButton_Click(Graphable able) { @@ -346,11 +454,46 @@ public partial class GraphForm : Form RegenerateMenuItems(); } + private readonly Dictionary sfDetailForms = []; + private void ChangeSlopeFieldDetail(SlopeField sf) + { + if (sfDetailForms.TryGetValue(sf, out SlopeFieldDetailForm? preexistingForm)) + { + preexistingForm.Focus(); + return; + } + + SlopeFieldDetailForm detailForm = new(this, sf) + { + StartPosition = FormStartPosition.Manual + }; + sfDetailForms.Add(sf, detailForm); + + detailForm.Location = new Point(Location.X + ClientRectangle.Width + 10, + Location.Y + (ClientRectangle.Height - detailForm.ClientRectangle.Height) / 2); + + if (detailForm.Location.X + detailForm.Width > Screen.FromControl(this).WorkingArea.Width) + { + detailForm.StartPosition = FormStartPosition.WindowsDefaultLocation; + } + detailForm.TopMost = true; + detailForm.Show(); + + detailForm.FormClosed += (o, e) => sfDetailForms.Remove(sf); + } + private void RegenerateMenuItems() { - MenuColors.DropDownItems.Clear(); - MenuEquationsDerivative.DropDownItems.Clear(); - MenuEquationsIntegral.DropDownItems.Clear(); + MenuElementsColors.DropDownItems.Clear(); + MenuElementsDetail.DropDownItems.Clear(); + MenuElementsRemove.DropDownItems.Clear(); + MenuOperationsDerivative.DropDownItems.Clear(); + MenuOperationsIntegral.DropDownItems.Clear(); + MenuConvertEquation.DropDownItems.Clear(); + MenuConvertSlopeField.DropDownItems.Clear(); + MenuOperationsTranslate.DropDownItems.Clear(); + // At some point, we'll have a Convert To Column Table button, + // but I'll need to make a form for the ranges when I do that. foreach (Graphable able in ables) { @@ -360,7 +503,26 @@ public partial class GraphForm : Form Text = able.Name }; colorItem.Click += (o, e) => GraphColorPickerButton_Click(able); - MenuColors.DropDownItems.Add(colorItem); + MenuElementsColors.DropDownItems.Add(colorItem); + + ToolStripMenuItem removeItem = new() + { + ForeColor = able.Color, + Text = able.Name + }; + removeItem.Click += (o, e) => Ungraph(able); + MenuElementsRemove.DropDownItems.Add(removeItem); + + if (able is SlopeField sf) + { + ToolStripMenuItem sfDetailItem = new() + { + ForeColor = able.Color, + Text = able.Name + }; + sfDetailItem.Click += (o, e) => ChangeSlopeFieldDetail(sf); + MenuElementsDetail.DropDownItems.Add(sfDetailItem); + } if (able is IDerivable derivable) { @@ -370,7 +532,7 @@ public partial class GraphForm : Form Text = able.Name }; derivativeItem.Click += (o, e) => Graph(derivable.Derive()); - MenuEquationsDerivative.DropDownItems.Add(derivativeItem); + MenuOperationsDerivative.DropDownItems.Add(derivativeItem); } if (able is IIntegrable integrable) { @@ -380,20 +542,76 @@ public partial class GraphForm : Form Text = able.Name }; integralItem.Click += (o, e) => Graph(integrable.Integrate()); - MenuEquationsIntegral.DropDownItems.Add(integralItem); + MenuOperationsIntegral.DropDownItems.Add(integralItem); + } + if (able is IConvertEquation equConvert) + { + ToolStripMenuItem equItem = new() + { + ForeColor = able.Color, + Text = able.Name + }; + equItem.Click += (o, e) => + { + if (equConvert.UngraphWhenConvertedToEquation) Ungraph(able); + Graph(equConvert.ToEquation()); + }; + MenuConvertEquation.DropDownItems.Add(equItem); + } + if (able is IConvertSlopeField sfConvert) + { + ToolStripMenuItem sfItem = new() + { + ForeColor = able.Color, + Text = able.Name + }; + sfItem.Click += (o, e) => + { + if (sfConvert.UngraphWhenConvertedToSlopeField) Ungraph(able); + Graph(sfConvert.ToSlopeField(2)); + }; + MenuConvertSlopeField.DropDownItems.Add(sfItem); + } + if (able is ITranslatable translatable) + { + ToolStripMenuItem transItem = new() + { + ForeColor = able.Color, + Text = able.Name + }; + transItem.Click += (o, e) => ElementsOperationsTranslate_Click(able, translatable); + MenuOperationsTranslate.DropDownItems.Add(transItem); } } } private void ButtonViewportSetZoom_Click(object? sender, EventArgs e) { - SetZoomForm picker = new(this) + if (setZoomForm is not null) + { + setZoomForm.Focus(); + return; + } + + SetZoomForm zoomForm = new(this) { StartPosition = FormStartPosition.Manual, }; - picker.Location = new Point(Location.X + ClientRectangle.Width + 10, - Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2); - picker.ShowDialog(); + zoomForm.Location = new Point(Location.X + ClientRectangle.Width + 10, + Location.Y + (ClientRectangle.Height - zoomForm.ClientRectangle.Height) / 2); + + if (zoomForm.Location.X + zoomForm.Width > Screen.FromControl(this).WorkingArea.Width) + { + zoomForm.StartPosition = FormStartPosition.WindowsDefaultLocation; + } + + setZoomForm = zoomForm; + zoomForm.Show(); + zoomForm.FormClosing += (o, e) => + { + zoomForm.CompleteBoxSelection(); + setZoomForm = null; + }; } private void ButtonViewportSetCenter_Click(object? sender, EventArgs e) { @@ -402,7 +620,7 @@ public partial class GraphForm : Form private void ButtonViewportReset_Click(object? sender, EventArgs e) { ScreenCenter = new Float2(0, 0); - ZoomLevel = 1; + ZoomLevel = new(1, 1); Invalidate(false); } private void ButtonViewportResetWindow_Click(object? sender, EventArgs e) @@ -412,12 +630,30 @@ public partial class GraphForm : Form WindowState = FormWindowState.Normal; } + public void ResetAllViewport() + { + ScreenCenter = new Float2(0, 0); + ZoomLevel = new(1, 1); + Location = initialWindowPos; + Size = initialWindowSize; + WindowState = FormWindowState.Normal; + Invalidate(false); + } + + private ViewCacheForm? cacheForm; private void MenuMiscCaches_Click(object? sender, EventArgs e) { + if (this.cacheForm is not null) + { + this.cacheForm.Focus(); + return; + } + ViewCacheForm cacheForm = new(this) { StartPosition = FormStartPosition.Manual }; + this.cacheForm = cacheForm; cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10, Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2); @@ -429,7 +665,7 @@ public partial class GraphForm : Form cacheForm.TopMost = true; cacheForm.Show(); } - private void MiscMenuPreload_Click(object sender, EventArgs e) + private void MiscMenuPreload_Click(object? sender, EventArgs e) { Float2 min = MinVisibleGraph, max = MaxVisibleGraph; Float2 add = new(max.x - min.x, max.y - min.y); @@ -446,4 +682,95 @@ public partial class GraphForm : Form foreach (Graphable able in Graphables) able.Preload(xRange, yRange, step); Invalidate(false); } + private void UpdaterPopupCloseButton_Click(object? sender, EventArgs e) + { + UpdaterPopup.Dispose(); + } + + private void ElementsOperationsTranslate_Click(Graphable ableRaw, ITranslatable ableTrans) + { + TranslateForm shifter = new(this, ableRaw, ableTrans) + { + StartPosition = FormStartPosition.Manual, + }; + shifter.Location = new Point(Location.X + ClientRectangle.Width + 10, + Location.Y + (ClientRectangle.Height - shifter.ClientRectangle.Height) / 2); + if (shifter.Location.X + shifter.Width > Screen.FromControl(this).WorkingArea.Width) + { + shifter.StartPosition = FormStartPosition.WindowsDefaultLocation; + } + shifter.Show(); + } + + private async void RunUpdateChecker() + { + try + { + HttpClient http = new(); + HttpRequestMessage request = new(HttpMethod.Get, "https://api.github.com/repos/That-One-Nerd/Graphing/releases"); + request.Headers.Add("User-Agent", "ThatOneNerd.Graphing-Update-Checker"); + + HttpResponseMessage result = await http.SendAsync(request); + if (!result.IsSuccessStatusCode) + { + Console.WriteLine($"Failed to check for updates."); + return; + } + + JsonArray arr = JsonSerializer.Deserialize(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()); + + 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(); + 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, + } } diff --git a/Base/Forms/SetZoomForm.Designer.cs b/Base/Forms/SetZoomForm.Designer.cs index 52990db..3d8fdaa 100644 --- a/Base/Forms/SetZoomForm.Designer.cs +++ b/Base/Forms/SetZoomForm.Designer.cs @@ -1,7 +1,4 @@ -using System.Drawing; -using System.Windows.Forms; - -namespace Graphing.Forms +namespace Graphing.Forms { partial class SetZoomForm { @@ -31,90 +28,157 @@ namespace Graphing.Forms /// private void InitializeComponent() { - MessageLabel = new Label(); - ZoomTrackBar = new TrackBar(); - ValueLabel = new Label(); - ZoomMinValue = new TextBox(); - ZoomMaxValue = new TextBox(); - ((System.ComponentModel.ISupportInitialize)ZoomTrackBar).BeginInit(); + EnableBoxSelect = new System.Windows.Forms.Button(); + MatchAspectButton = new System.Windows.Forms.Button(); + ResetButton = new System.Windows.Forms.Button(); + NormalizeButton = new System.Windows.Forms.Button(); + MinBoxX = new System.Windows.Forms.TextBox(); + TextX = new System.Windows.Forms.Label(); + MaxBoxX = new System.Windows.Forms.TextBox(); + MaxBoxY = new System.Windows.Forms.TextBox(); + TextY = new System.Windows.Forms.Label(); + MinBoxY = new System.Windows.Forms.TextBox(); + ViewportLock = new System.Windows.Forms.CheckBox(); SuspendLayout(); // - // MessageLabel + // EnableBoxSelect // - MessageLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - MessageLabel.Location = new Point(52, 20); - MessageLabel.Name = "MessageLabel"; - MessageLabel.Size = new Size(413, 35); - MessageLabel.TabIndex = 0; - MessageLabel.Text = "Set the zoom level for the graph."; - MessageLabel.TextAlign = ContentAlignment.MiddleCenter; + EnableBoxSelect.Location = new System.Drawing.Point(12, 12); + EnableBoxSelect.Name = "EnableBoxSelect"; + EnableBoxSelect.Size = new System.Drawing.Size(187, 46); + EnableBoxSelect.TabIndex = 0; + EnableBoxSelect.Text = "Box Select"; + EnableBoxSelect.UseVisualStyleBackColor = true; + EnableBoxSelect.Click += EnableBoxSelect_Click; // - // ZoomTrackBar + // MatchAspectButton // - ZoomTrackBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - ZoomTrackBar.LargeChange = 1000; - ZoomTrackBar.Location = new Point(12, 127); - ZoomTrackBar.Maximum = 10000; - ZoomTrackBar.Name = "ZoomTrackBar"; - ZoomTrackBar.Size = new Size(489, 90); - ZoomTrackBar.TabIndex = 1; - ZoomTrackBar.TickStyle = TickStyle.None; - ZoomTrackBar.Scroll += ZoomTrackBar_Scroll; + MatchAspectButton.Location = new System.Drawing.Point(12, 64); + MatchAspectButton.Name = "MatchAspectButton"; + MatchAspectButton.Size = new System.Drawing.Size(187, 46); + MatchAspectButton.TabIndex = 1; + MatchAspectButton.Text = "Match Aspect"; + MatchAspectButton.UseVisualStyleBackColor = true; + MatchAspectButton.Click += MatchAspectButton_Click; // - // ValueLabel + // ResetButton // - ValueLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - ValueLabel.Location = new Point(52, 91); - ValueLabel.Name = "ValueLabel"; - ValueLabel.Size = new Size(413, 33); - ValueLabel.TabIndex = 2; - ValueLabel.Text = "1.00x"; - ValueLabel.TextAlign = ContentAlignment.TopCenter; + ResetButton.Location = new System.Drawing.Point(12, 168); + ResetButton.Name = "ResetButton"; + ResetButton.Size = new System.Drawing.Size(187, 46); + ResetButton.TabIndex = 2; + ResetButton.Text = "Reset"; + ResetButton.UseVisualStyleBackColor = true; + ResetButton.Click += ResetButton_Click; // - // ZoomMinValue + // NormalizeButton // - ZoomMinValue.Location = new Point(12, 178); - ZoomMinValue.Name = "ZoomMinValue"; - ZoomMinValue.Size = new Size(83, 39); - ZoomMinValue.TabIndex = 3; - ZoomMinValue.Text = "0.50"; - ZoomMinValue.TextChanged += ZoomMinValue_TextChanged; + NormalizeButton.Location = new System.Drawing.Point(12, 116); + NormalizeButton.Name = "NormalizeButton"; + NormalizeButton.Size = new System.Drawing.Size(187, 46); + NormalizeButton.TabIndex = 3; + NormalizeButton.Text = "Normalize"; + NormalizeButton.UseVisualStyleBackColor = true; + NormalizeButton.Click += NormalizeButton_Click; // - // ZoomMaxValue + // MinBoxX // - ZoomMaxValue.Anchor = AnchorStyles.Top | AnchorStyles.Right; - ZoomMaxValue.Location = new Point(418, 178); - ZoomMaxValue.Name = "ZoomMaxValue"; - ZoomMaxValue.Size = new Size(83, 39); - ZoomMaxValue.TabIndex = 4; - ZoomMaxValue.Text = "2.00"; - ZoomMaxValue.TextAlign = HorizontalAlignment.Right; - ZoomMaxValue.TextChanged += ZoomMaxValue_TextChanged; + MinBoxX.Location = new System.Drawing.Point(227, 49); + MinBoxX.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3); + MinBoxX.Name = "MinBoxX"; + MinBoxX.Size = new System.Drawing.Size(108, 39); + MinBoxX.TabIndex = 4; + // + // TextX + // + TextX.Location = new System.Drawing.Point(335, 49); + TextX.Margin = new System.Windows.Forms.Padding(0); + TextX.Name = "TextX"; + TextX.Size = new System.Drawing.Size(77, 39); + TextX.TabIndex = 5; + TextX.Text = "≤ x ≤"; + TextX.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // MaxBoxX + // + MaxBoxX.Location = new System.Drawing.Point(412, 49); + MaxBoxX.Margin = new System.Windows.Forms.Padding(0, 3, 25, 3); + MaxBoxX.Name = "MaxBoxX"; + MaxBoxX.Size = new System.Drawing.Size(108, 39); + MaxBoxX.TabIndex = 6; + // + // MaxBoxY + // + MaxBoxY.Location = new System.Drawing.Point(412, 94); + MaxBoxY.Margin = new System.Windows.Forms.Padding(0, 3, 25, 3); + MaxBoxY.Name = "MaxBoxY"; + MaxBoxY.Size = new System.Drawing.Size(108, 39); + MaxBoxY.TabIndex = 9; + // + // TextY + // + TextY.Location = new System.Drawing.Point(335, 94); + TextY.Margin = new System.Windows.Forms.Padding(0); + TextY.Name = "TextY"; + TextY.Size = new System.Drawing.Size(77, 39); + TextY.TabIndex = 8; + TextY.Text = "≤ y ≤"; + TextY.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // MinBoxY + // + MinBoxY.Location = new System.Drawing.Point(227, 94); + MinBoxY.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3); + MinBoxY.Name = "MinBoxY"; + MinBoxY.Size = new System.Drawing.Size(108, 39); + MinBoxY.TabIndex = 7; + // + // ViewportLock + // + ViewportLock.Location = new System.Drawing.Point(227, 139); + ViewportLock.Name = "ViewportLock"; + ViewportLock.Size = new System.Drawing.Size(293, 39); + ViewportLock.TabIndex = 10; + ViewportLock.Text = "Lock Viewport"; + ViewportLock.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + ViewportLock.UseVisualStyleBackColor = true; + ViewportLock.CheckedChanged += ViewportLock_CheckedChanged; // // SetZoomForm // - AutoScaleDimensions = new SizeF(13F, 32F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(513, 230); - Controls.Add(ZoomMaxValue); - Controls.Add(ZoomMinValue); - Controls.Add(ValueLabel); - Controls.Add(ZoomTrackBar); - Controls.Add(MessageLabel); - FormBorderStyle = FormBorderStyle.FixedToolWindow; + AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(533, 227); + Controls.Add(ViewportLock); + Controls.Add(MaxBoxY); + Controls.Add(TextY); + Controls.Add(MinBoxY); + Controls.Add(MaxBoxX); + Controls.Add(TextX); + Controls.Add(MinBoxX); + Controls.Add(NormalizeButton); + Controls.Add(ResetButton); + Controls.Add(MatchAspectButton); + Controls.Add(EnableBoxSelect); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; Name = "SetZoomForm"; - Text = "Zoom Level"; - ((System.ComponentModel.ISupportInitialize)ZoomTrackBar).EndInit(); + Text = "Set Viewport Zoom"; ResumeLayout(false); PerformLayout(); } #endregion - private Label MessageLabel; - private TrackBar ZoomTrackBar; - private Label ValueLabel; - private TextBox ZoomMinValue; - private TextBox ZoomMaxValue; + private System.Windows.Forms.Button EnableBoxSelect; + private System.Windows.Forms.Button MatchAspectButton; + private System.Windows.Forms.Button ResetButton; + private System.Windows.Forms.Button NormalizeButton; + private System.Windows.Forms.TextBox MinBoxX; + private System.Windows.Forms.Label TextX; + private System.Windows.Forms.TextBox MaxBoxX; + private System.Windows.Forms.TextBox MaxBoxY; + private System.Windows.Forms.Label TextY; + private System.Windows.Forms.TextBox MinBoxY; + private System.Windows.Forms.CheckBox ViewportLock; } } \ No newline at end of file diff --git a/Base/Forms/SetZoomForm.cs b/Base/Forms/SetZoomForm.cs index fc21296..939a7fc 100644 --- a/Base/Forms/SetZoomForm.cs +++ b/Base/Forms/SetZoomForm.cs @@ -5,118 +5,223 @@ namespace Graphing.Forms; public partial class SetZoomForm : Form { - private double minZoomRange; - private double maxZoomRange; + private readonly GraphForm refForm; - private double zoomLevel; + private bool boxSelectEnabled; - private readonly GraphForm form; - - public SetZoomForm(GraphForm form) + public SetZoomForm(GraphForm refForm) { InitializeComponent(); + this.refForm = refForm; - minZoomRange = 1 / (form.ZoomLevel * 2); - maxZoomRange = 2 / form.ZoomLevel; - zoomLevel = 1 / form.ZoomLevel; + refForm.Paint += (o, e) => RedeclareValues(); + RedeclareValues(); - ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum); - - this.form = form; - } - - protected override void OnPaint(PaintEventArgs e) - { - ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); - ZoomMinValue.Text = minZoomRange.ToString("0.00"); - - ValueLabel.Text = $"{zoomLevel:0.00}x"; - - base.OnPaint(e); - - form.ZoomLevel = 1 / zoomLevel; - form.Invalidate(false); - } - - private double FactorToZoom(double factor) - { - return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange); - } - private double ZoomToFactor(double zoom) - { - double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange); - return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue)); - } - - private void ZoomTrackBar_Scroll(object? sender, EventArgs e) - { - double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); - zoomLevel = FactorToZoom(factor); - - Invalidate(true); - } - - private void ZoomMinValue_TextChanged(object? sender, EventArgs e) - { - double original = minZoomRange; - try + MinBoxX.Leave += MinBoxX_Finish; + MinBoxX.KeyDown += (o, e) => { - double value; - if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) || - ZoomMinValue.Text.EndsWith('.')) - { - return; - } - else - { - value = double.Parse(ZoomMinValue.Text); - if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new(); - } + if (e.KeyCode == Keys.Enter) MinBoxX_Finish(o, e); + }; + MaxBoxX.Leave += MaxBoxX_Finish; + MaxBoxX.KeyDown += (o, e) => + { + if (e.KeyCode == Keys.Enter) MaxBoxX_Finish(o, e); + }; - minZoomRange = value; - ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); - double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); - double newZoom = FactorToZoom(factor); + MinBoxY.Leave += MinBoxY_Finish; + MinBoxY.KeyDown += (o, e) => + { + if (e.KeyCode == Keys.Enter) MinBoxY_Finish(o, e); + }; + MaxBoxY.Leave += MaxBoxY_Finish; + MaxBoxY.KeyDown += (o, e) => + { + if (e.KeyCode == Keys.Enter) MaxBoxY_Finish(o, e); + }; + } - zoomLevel = newZoom; - if (newZoom != factor) Invalidate(true); + private void EnableBoxSelect_Click(object? sender, EventArgs e) + { + boxSelectEnabled = !boxSelectEnabled; + refForm.canBoxSelect = boxSelectEnabled; + + if (boxSelectEnabled) + { + EnableBoxSelect.Text = $"Cancel ..."; + refForm.Focus(); } - catch + else { - minZoomRange = original; - ZoomMinValue.Text = minZoomRange.ToString("0.00"); + EnableBoxSelect.Text = "Box Select"; } } - - private void ZoomMaxValue_TextChanged(object sender, EventArgs e) + private void MatchAspectButton_Click(object? sender, EventArgs e) { - double original = maxZoomRange; - try + double zoomXFactor = refForm.ZoomLevel.x / refForm.ZoomLevel.y; + double actualXFactor = refForm.ClientRectangle.Width / refForm.ClientRectangle.Height; + + double diff = actualXFactor / zoomXFactor; + int newWidth = (int)(refForm.Width / diff); + refForm.ZoomLevel = new(refForm.ZoomLevel.x * diff, refForm.ZoomLevel.y); + + int maxScreenWidth = Screen.FromControl(refForm).WorkingArea.Width; + if (newWidth >= maxScreenWidth) { - double value; - if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) || - ZoomMaxValue.Text.EndsWith('.')) + refForm.Location = new(0, refForm.Location.Y); + + double xScaleFactor = (double)maxScreenWidth / newWidth; + newWidth = maxScreenWidth; + refForm.Height = (int)(refForm.Height * xScaleFactor); + refForm.ZoomLevel = new(refForm.ZoomLevel.x * xScaleFactor, refForm.ZoomLevel.y * xScaleFactor); + } + + refForm.Width = newWidth; + } + private void NormalizeButton_Click(object? sender, EventArgs e) + { + double factor = 1 / Math.Min(refForm.ZoomLevel.x, refForm.ZoomLevel.y); + refForm.ZoomLevel = new(factor * refForm.ZoomLevel.x, factor * refForm.ZoomLevel.y); + } + private void ResetButton_Click(object? sender, EventArgs e) + { + refForm.ResetAllViewport(); + } + private void ViewportLock_CheckedChanged(object? sender, EventArgs e) + { + refForm.ViewportLocked = ViewportLock.Checked; + RedeclareValues(); + } + + private void MinBoxX_Finish(object? sender, EventArgs e) + { + if (double.TryParse(MinBoxX.Text, out double minX)) + { + Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph; + + if (minX > max.x) { - return; - } - else - { - value = double.Parse(ZoomMaxValue.Text); - if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new(); + MaxBoxX.Text = MinBoxX.Text; + MaxBoxX_Finish(sender, e); + minX = max.x; + + // Redefine bounds. + min = refForm.MinVisibleGraph; + max = refForm.MaxVisibleGraph; } - maxZoomRange = value; - ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); - double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); - double newZoom = FactorToZoom(factor); + double newCenterX = (minX + max.x) / 2, + zoomFactorX = (max.x - minX) / (max.x - min.x); - zoomLevel = newZoom; - if (newZoom != factor) Invalidate(true); + refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y); + refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y); } - catch + + refForm.Invalidate(false); + } + private void MaxBoxX_Finish(object? sender, EventArgs e) + { + if (double.TryParse(MaxBoxX.Text, out double maxX)) { - maxZoomRange = original; - ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); + Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph; + + if (maxX < min.x) + { + MinBoxX.Text = MaxBoxX.Text; + MinBoxX_Finish(sender, e); + maxX = min.x; + + // Redefine bounds. + min = refForm.MinVisibleGraph; + max = refForm.MaxVisibleGraph; + } + + double newCenterX = (min.x + maxX) / 2, + zoomFactorX = (maxX - min.x) / (max.x - min.x); + + refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y); + refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y); } + + refForm.Invalidate(false); + } + private void MinBoxY_Finish(object? sender, EventArgs e) + { + if (double.TryParse(MinBoxY.Text, out double minY)) + { + Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph; + + if (minY > max.y) + { + MaxBoxY.Text = MinBoxY.Text; + MaxBoxY_Finish(sender, e); + minY = max.y; + + // Redefine bounds. + min = refForm.MinVisibleGraph; + max = refForm.MaxVisibleGraph; + } + + double newCenterY = -(minY + max.y) / 2, // Keeping it positive flips it for some reason ??? + zoomFactorY = (max.y - minY) / (max.y - min.y); + + refForm.ScreenCenter = new(refForm.ScreenCenter.x, newCenterY); + refForm.ZoomLevel = new(refForm.ZoomLevel.x, refForm.ZoomLevel.y * zoomFactorY); + } + + refForm.Invalidate(false); + } + private void MaxBoxY_Finish(object? sender, EventArgs e) + { + if (double.TryParse(MaxBoxY.Text, out double maxY)) + { + Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph; + + if (maxY < min.y) + { + MinBoxY.Text = MaxBoxY.Text; + MinBoxY_Finish(sender, e); + maxY = min.y; + + // Redefine bounds. + min = refForm.MinVisibleGraph; + max = refForm.MaxVisibleGraph; + } + + double newCenterY = -(min.y + maxY) / 2, // Keeping it positive flips it for some reason ??? + zoomFactorY = (maxY - min.y) / (max.y - min.y); + + refForm.ScreenCenter = new(refForm.ScreenCenter.x, newCenterY); + refForm.ZoomLevel = new(refForm.ZoomLevel.x, refForm.ZoomLevel.y * zoomFactorY); + } + + refForm.Invalidate(false); + } + + public void RedeclareValues() + { + bool enabled = !refForm.ViewportLocked; + + Float2 minGraph = refForm.MinVisibleGraph, + maxGraph = refForm.MaxVisibleGraph; + + MinBoxX.Text = $"{minGraph.x:0.000}"; + MaxBoxX.Text = $"{maxGraph.x:0.000}"; + MinBoxY.Text = $"{minGraph.y:0.000}"; + MaxBoxY.Text = $"{maxGraph.y:0.000}"; + + ViewportLock.Checked = !enabled; + EnableBoxSelect.Enabled = enabled; + MatchAspectButton.Enabled = enabled; + NormalizeButton.Enabled = enabled; + ResetButton.Enabled = enabled; + MinBoxX.Enabled = enabled; + MaxBoxX.Enabled = enabled; + MinBoxY.Enabled = enabled; + MaxBoxY.Enabled = enabled; + } + + internal void CompleteBoxSelection() + { + if (boxSelectEnabled) EnableBoxSelect_Click(null, new()); } } diff --git a/Base/Forms/SlopeFieldDetailForm.Designer.cs b/Base/Forms/SlopeFieldDetailForm.Designer.cs new file mode 100644 index 0000000..1d93ed2 --- /dev/null +++ b/Base/Forms/SlopeFieldDetailForm.Designer.cs @@ -0,0 +1,147 @@ +namespace Graphing.Forms +{ + partial class SlopeFieldDetailForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + 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; + } +} \ No newline at end of file diff --git a/Base/Forms/SlopeFieldDetailForm.cs b/Base/Forms/SlopeFieldDetailForm.cs new file mode 100644 index 0000000..20575c7 --- /dev/null +++ b/Base/Forms/SlopeFieldDetailForm.cs @@ -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); + } +} diff --git a/Base/Forms/SlopeFieldDetailForm.resx b/Base/Forms/SlopeFieldDetailForm.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/Base/Forms/SlopeFieldDetailForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Base/Forms/TranslateForm.Designer.cs b/Base/Forms/TranslateForm.Designer.cs new file mode 100644 index 0000000..2f9f7c6 --- /dev/null +++ b/Base/Forms/TranslateForm.Designer.cs @@ -0,0 +1,204 @@ +namespace Graphing.Forms +{ + partial class TranslateForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + 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; + } +} \ No newline at end of file diff --git a/Base/Forms/TranslateForm.cs b/Base/Forms/TranslateForm.cs new file mode 100644 index 0000000..d0ac022 --- /dev/null +++ b/Base/Forms/TranslateForm.cs @@ -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); + } +} diff --git a/Base/Forms/TranslateForm.resx b/Base/Forms/TranslateForm.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/Base/Forms/TranslateForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Base/Forms/ViewCacheForm.cs b/Base/Forms/ViewCacheForm.cs index 7f767a9..4fa824b 100644 --- a/Base/Forms/ViewCacheForm.cs +++ b/Base/Forms/ViewCacheForm.cs @@ -33,6 +33,7 @@ public partial class ViewCacheForm : Form foreach (Graphable able in refForm.Graphables) { long thisBytes = able.GetCacheBytes(); + if (thisBytes == 0) continue; CachePie.Values.Add((able.Color, thisBytes)); totalBytes += thisBytes; diff --git a/Base/Graphable.cs b/Base/Graphable.cs index 2844ef7..75f2d8a 100644 --- a/Base/Graphable.cs +++ b/Base/Graphable.cs @@ -30,12 +30,12 @@ public abstract class Graphable public abstract IEnumerable GetItemsToRender(in GraphForm graph); - public abstract Graphable DeepCopy(); + public abstract Graphable ShallowCopy(); public virtual void EraseCache() { } public virtual long GetCacheBytes() => 0; public virtual void Preload(Float2 xRange, Float2 yRange, double step) { } public virtual bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor) => false; - public virtual Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => default; + public virtual IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) => []; } diff --git a/Base/Graphables/ColumnTable.cs b/Base/Graphables/ColumnTable.cs index 0461cc3..da154fe 100644 --- a/Base/Graphables/ColumnTable.cs +++ b/Base/Graphables/ColumnTable.cs @@ -2,6 +2,7 @@ using Graphing.Parts; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; namespace Graphing.Graphables; @@ -23,6 +24,7 @@ public class ColumnTable : Graphable } public ColumnTable(double step, Equation equation, double min, double max) { + Color = equation.Color; Name = $"Column Table for {equation.Name}"; tableXY = []; @@ -37,7 +39,7 @@ public class ColumnTable : Graphable public override long GetCacheBytes() => 16 * tableXY.Count; - public override Graphable DeepCopy() => new ColumnTable(width / 0.75, tableXY.ToArray().ToDictionary()); + public override Graphable ShallowCopy() => new ColumnTable(width / 0.75, tableXY); public override IEnumerable GetItemsToRender(in GraphForm graph) { @@ -45,12 +47,87 @@ public class ColumnTable : Graphable foreach (KeyValuePair col in tableXY) { items.Add(GraphRectangle.FromSize(new Float2(col.Key, col.Value / 2), - new Float2(width, col.Value))); + new Float2(width, col.Value), 0.625)); } return items; } + public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor) + { + // Get closest value to mouse pos. + double closestDist = double.PositiveInfinity, closestX = 0, closestY = 0; + foreach (KeyValuePair 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 GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) + { + // Get closest value to mouse pos. + double closestDist = double.PositiveInfinity, closestX = 0, closestY = 0; + foreach (KeyValuePair points in tableXY) + { + double dist = Math.Abs(points.Key - graphMousePos.x); + if (dist < closestDist) + { + closestDist = dist; + closestX = points.Key; + closestY = points.Value; + } + } + + Float2 textPoint = new(closestX, closestY); + Int2 offset; + ContentAlignment alignment; + if (textPoint.y >= 0) + { + offset = new(0, -5); + alignment = ContentAlignment.BottomCenter; + } + else + { + offset = new(0, 5); + alignment = ContentAlignment.TopCenter; + } + + return + [ + new GraphUiText($"{closestY:0.00}", textPoint, alignment, offsetPix: offset) + ]; + } + // Nothing to preload, everything is already cached. public override void Preload(Float2 xRange, Float2 yRange, double step) { } } diff --git a/Base/Graphables/Equation.cs b/Base/Graphables/Equation.cs index c3dfa4e..e28deee 100644 --- a/Base/Graphables/Equation.cs +++ b/Base/Graphables/Equation.cs @@ -3,16 +3,26 @@ using Graphing.Forms; using Graphing.Parts; using System; using System.Collections.Generic; +using System.Drawing; namespace Graphing.Graphables; -public class Equation : Graphable, IIntegrable, IDerivable +public class Equation : Graphable, IIntegrable, IDerivable, ITranslatableXY, IConvertSlopeField, + IConvertColumnTable { private static int equationNum; + public bool UngraphWhenConvertedToColumnTable => false; + public bool UngraphWhenConvertedToSlopeField => false; + + public double OffsetX { get; set; } + public double OffsetY { get; set; } + protected readonly EquationDelegate equ; protected readonly List cache; + public event Action OnInvalidate; + public Equation(EquationDelegate equ) { equationNum++; @@ -20,6 +30,11 @@ public class Equation : Graphable, IIntegrable, IDerivable this.equ = equ; cache = []; + + OffsetX = 0; + OffsetY = 0; + + OnInvalidate = delegate { }; } public override IEnumerable GetItemsToRender(in GraphForm graph) @@ -46,31 +61,44 @@ public class Equation : Graphable, IIntegrable, IDerivable previousX = currentX; previousY = currentY; } + OnInvalidate.Invoke(graph); return lines; } - public Graphable Derive() => new Equation(x => + protected double DerivativeAtPoint(double x) { const double step = 1e-3; - return (equ(x + step) - equ(x)) / step; - }); + return (equ(x + step - OffsetX) - equ(x - OffsetX)) / step; + } + + public Graphable Derive() => new Equation(DerivativeAtPoint); public Graphable Integrate() => new IntegralEquation(this); public EquationDelegate GetDelegate() => equ; + public SlopeField ToSlopeField(int detail) => new(detail, (x, y) => DerivativeAtPoint(x)) + { + Color = Color, + Name = $"Slope Field of {Name}" + }; + public ColumnTable ToColumnTable(double start, double end, int detail) + => new(1.0 / detail, this, start, end); + public override void EraseCache() => cache.Clear(); protected double GetFromCache(double x, double epsilon) { - (double dist, double nearest, int index) = NearestCachedPoint(x); - if (dist < epsilon) return nearest; + (double dist, double nearest, int index) = NearestCachedPoint(x - OffsetX); + if (dist < epsilon) return nearest + OffsetY; else { - double result = equ(x); - cache.Insert(index + 1, new(x, result)); - return result; + double result = equ(x - OffsetX); + cache.Insert(index + 1, new(x - OffsetX, result)); + return result + OffsetY; } } + public double GetValueAt(double x) => GetFromCache(x, 0); + protected (double dist, double y, int index) NearestCachedPoint(double x) { if (cache.Count == 0) return (double.PositiveInfinity, double.NaN, -1); @@ -103,7 +131,7 @@ public class Equation : Graphable, IIntegrable, IDerivable } } - public override Graphable DeepCopy() => new Equation(equ); + public override Graphable ShallowCopy() => new Equation(equ); public override long GetCacheBytes() => cache.Count * 16; @@ -121,8 +149,15 @@ public class Equation : Graphable, IIntegrable, IDerivable double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y); return totalDist <= allowedDist; } - public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => - new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3)); + public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) + { + Float2 point = new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3)); + return + [ + new GraphUiText($"({point.x:0.00}, {point.y:0.00})", point, ContentAlignment.BottomLeft), + new GraphUiCircle(point), + ]; + } public override void Preload(Float2 xRange, Float2 yRange, double step) { diff --git a/Base/Graphables/EquationDifference.cs b/Base/Graphables/EquationDifference.cs new file mode 100644 index 0000000..ebe02c7 --- /dev/null +++ b/Base/Graphables/EquationDifference.cs @@ -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 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 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 + }; +} diff --git a/Base/Graphables/IntegralEquation.cs b/Base/Graphables/IntegralEquation.cs index 51742fd..daf28d5 100644 --- a/Base/Graphables/IntegralEquation.cs +++ b/Base/Graphables/IntegralEquation.cs @@ -47,7 +47,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable usingAlt = true; } - public override Graphable DeepCopy() => new IntegralEquation(this); + public override Graphable ShallowCopy() => new IntegralEquation(this); public override IEnumerable GetItemsToRender(in GraphForm graph) { @@ -164,7 +164,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable } else { - stepY += baseEquDel!(stepX) * dX; + stepY += (baseEquDel!(stepX - baseEqu!.OffsetX) + baseEqu.OffsetY) * dX; } } @@ -178,8 +178,8 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable public Graphable Derive() { - if (usingAlt) return altBaseEqu!.DeepCopy(); - else return (Equation)baseEqu!.DeepCopy(); + if (usingAlt) return altBaseEqu!.ShallowCopy(); + else return (Equation)baseEqu!.ShallowCopy(); } public Graphable Integrate() => new IntegralEquation(this); @@ -234,6 +234,6 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y); return totalDist <= allowedDist; } - public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => - new(graphMousePos.x, IntegralAtPoint(graphMousePos.x)); + public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) => + [new GraphUiCircle(new(graphMousePos.x, IntegralAtPoint(graphMousePos.x)))]; } diff --git a/Base/Graphables/ParametricEquation.cs b/Base/Graphables/ParametricEquation.cs new file mode 100644 index 0000000..24f7f78 --- /dev/null +++ b/Base/Graphables/ParametricEquation.cs @@ -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 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 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); diff --git a/Base/Graphables/SlopeField.cs b/Base/Graphables/SlopeField.cs index 066f658..44b877e 100644 --- a/Base/Graphables/SlopeField.cs +++ b/Base/Graphables/SlopeField.cs @@ -2,6 +2,7 @@ using Graphing.Parts; using System; using System.Collections.Generic; +using System.Drawing; namespace Graphing.Graphables; @@ -9,29 +10,50 @@ public class SlopeField : Graphable { private static int slopeFieldNum; - protected readonly SlopeFieldsDelegate equ; - protected readonly int detail; + public double Detail + { + get => _detail; + set + { + if (Math.Abs(value - Detail) >= 1e-4) + { + // When changing detail, we need to regenerate all + // the lines. Inefficient, I know. Might be optimized + // in a future update. + EraseCache(); + } + _detail = value; + } + } + private double _detail; + protected readonly SlopeFieldsDelegate equ; protected readonly List<(Float2, GraphLine)> cache; - public SlopeField(int detail, SlopeFieldsDelegate equ) + public SlopeField(double detail, SlopeFieldsDelegate equ) { slopeFieldNum++; Name = $"Slope Field {slopeFieldNum}"; this.equ = equ; - this.detail = detail; + _detail = detail; cache = []; } public override IEnumerable GetItemsToRender(in GraphForm graph) { - double epsilon = 1 / (detail * 2.0); + double step = 1 / _detail; + double epsilon = step * 0.5; List lines = []; - for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1.0 / detail) + double minX = Math.Round((graph.MinVisibleGraph.x - 1) / step) * step, + maxX = Math.Round((graph.MaxVisibleGraph.x + 1) / step) * step, + minY = Math.Round((graph.MinVisibleGraph.y - 1) / step) * step, + maxY = Math.Round((graph.MaxVisibleGraph.y + 1) / step) * step; + + for (double x = minX; x < maxX; x += step) { - for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1.0 / detail) + for (double y = minY; y < maxY; y += step) { lines.Add(GetFromCache(epsilon, x, y)); } @@ -42,7 +64,7 @@ public class SlopeField : Graphable protected GraphLine MakeSlopeLine(Float2 position, double slope) { - double size = detail; + double size = _detail; double dirX = size, dirY = slope * size; double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY); @@ -72,17 +94,17 @@ public class SlopeField : Graphable return result; } - public override Graphable DeepCopy() => new SlopeField(detail, equ); + public override Graphable ShallowCopy() => new SlopeField(_detail, equ); public override void EraseCache() => cache.Clear(); public override long GetCacheBytes() => cache.Count * 48; public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor) { - Float2 nearestPos = new(Math.Round(graphMousePos.x * detail) / detail, - Math.Round(graphMousePos.y * detail) / detail); + Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail, + Math.Round(graphMousePos.y * _detail) / _detail); - double epsilon = 1 / (detail * 2.0); + double epsilon = 1 / (_detail * 2.0); GraphLine line = GetFromCache(epsilon, nearestPos.x, nearestPos.y); double slope = (line.b.y - line.a.y) / (line.b.x - line.a.x); @@ -101,12 +123,12 @@ public class SlopeField : Graphable double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y); return totalDist <= allowedDist; } - public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) + public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) { - Float2 nearestPos = new(Math.Round(graphMousePos.x * detail) / detail, - Math.Round(graphMousePos.y * detail) / detail); + Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail, + Math.Round(graphMousePos.y * _detail) / _detail); - double epsilon = 1 / (detail * 2.0); + double epsilon = 1 / (_detail * 2.0); GraphLine line = GetFromCache(epsilon, nearestPos.x, nearestPos.y); double slope = (line.b.y - line.a.y) / (line.b.x - line.a.x); @@ -114,14 +136,18 @@ public class SlopeField : Graphable lineY = slope * (lineX - nearestPos.x) + nearestPos.y; Float2 point = new(lineX, lineY); - return point; + return + [ + new GraphUiText($"M = {slope:0.000}", point, ContentAlignment.BottomLeft), + new GraphUiCircle(point) + ]; } public override void Preload(Float2 xRange, Float2 yRange, double step) { - for (double x = Math.Ceiling(xRange.x - 1); x < xRange.y + 1; x += 1.0 / detail) + for (double x = Math.Ceiling(xRange.x - 1); x < xRange.y + 1; x += 1.0 / _detail) { - for (double y = Math.Ceiling(yRange.x - 1); y < yRange.y + 1; y += 1.0 / detail) + for (double y = Math.Ceiling(yRange.x - 1); y < yRange.y + 1; y += 1.0 / _detail) { GetFromCache(step, x, y); } diff --git a/Base/Graphables/TangentLine.cs b/Base/Graphables/TangentLine.cs index f22eec0..10faab3 100644 --- a/Base/Graphables/TangentLine.cs +++ b/Base/Graphables/TangentLine.cs @@ -1,12 +1,16 @@ -using Graphing.Forms; +using Graphing.Abstract; +using Graphing.Forms; using Graphing.Parts; using System; using System.Collections.Generic; +using System.Drawing; namespace Graphing.Graphables; -public class TangentLine : Graphable +public class TangentLine : Graphable, IConvertEquation, ITranslatableX { + public bool UngraphWhenConvertedToEquation => true; + public double Position { get => _position; @@ -18,8 +22,13 @@ public class TangentLine : Graphable } private double _position; // Private because it has exactly the same functionality as `Position`. + public double OffsetX + { + get => Position; + set => Position = value; + } + protected readonly Equation parent; - protected readonly EquationDelegate parentEqu; protected readonly double length; @@ -35,16 +44,26 @@ public class TangentLine : Graphable Name = $"Tangent Line of {parent.Name}"; slopeCache = []; - parentEqu = parent.GetDelegate(); - Position = position; this.length = length; this.parent = parent; + Position = position; + + parent.OnInvalidate += (graph) => + { + // I don't love this but it works. + EraseCache(); + Position = _position; // Done for side effects. + }; } public override IEnumerable GetItemsToRender(in GraphForm graph) { Float2 point = new(Position, currentSlope.y); - return [MakeSlopeLine(), new GraphUiCircle(point, 8)]; + return + [ + MakeSlopeLine(), + new GraphUiCircle(point) + ]; } protected GraphLine MakeSlopeLine() { @@ -63,13 +82,13 @@ public class TangentLine : Graphable const double step = 1e-3; - double initial = parentEqu(x); - Float2 result = new((parentEqu(x + step) - initial) / step, initial); + double initial = parent.GetValueAt(x); + Float2 result = new((parent.GetValueAt(x + step) - initial) / step, initial); slopeCache.Add(x, result); return result; } - public override Graphable DeepCopy() => new TangentLine(length, Position, parent); + public override Graphable ShallowCopy() => new TangentLine(length, Position, parent); public override void EraseCache() => slopeCache.Clear(); public override long GetCacheBytes() => slopeCache.Count * 24; @@ -93,7 +112,7 @@ public class TangentLine : Graphable double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y); return totalDist <= allowedDist; } - public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) + public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) { GraphLine line = MakeSlopeLine(); @@ -101,7 +120,15 @@ public class TangentLine : Graphable Math.Min(line.a.x, line.b.x), Math.Max(line.a.x, line.b.x)), lineY = currentSlope.x * (lineX - Position) + currentSlope.y; - return new Float2(lineX, lineY); + + double slope = currentSlope.x; + Float2 point = new(lineX, lineY); + + return + [ + new GraphUiText($"M = {slope:0.000}", point, ContentAlignment.BottomLeft), + new GraphUiCircle(new(lineX, lineY)) + ]; } public override void Preload(Float2 xRange, Float2 yRange, double step) @@ -112,4 +139,14 @@ public class TangentLine : Graphable // that can be changed. for (double x = xRange.x; x <= xRange.y; x += step) DerivativeAtPoint(x); } + + public Equation ToEquation() + { + double slope = currentSlope.x, x1 = Position, y1 = currentSlope.y; + return new(x => slope * (x - x1) + y1) + { + Name = Name, + Color = Color + }; + } } diff --git a/Base/Parts/GraphRectangle.cs b/Base/Parts/GraphRectangle.cs index 4881397..1d4cac6 100644 --- a/Base/Parts/GraphRectangle.cs +++ b/Base/Parts/GraphRectangle.cs @@ -6,24 +6,28 @@ namespace Graphing.Parts; public record struct GraphRectangle : IGraphPart { public Float2 min, max; + public double opacity; public GraphRectangle() { min = new(); max = new(); + opacity = 1; } - public static GraphRectangle FromSize(Float2 center, Float2 size) => new() + public static GraphRectangle FromSize(Float2 center, Float2 size, double opacity = 1) => new() { min = new(center.x - size.x / 2, center.y - size.y / 2), max = new(center.x + size.x / 2, - center.y + size.y / 2) + center.y + size.y / 2), + opacity = opacity, }; - public static GraphRectangle FromRange(Float2 min, Float2 max) => new() + public static GraphRectangle FromRange(Float2 min, Float2 max, double opacity = 1) => new() { min = min, - max = max + max = max, + opacity = opacity, }; public void Render(in GraphForm form, in Graphics g, in Pen pen) @@ -41,6 +45,9 @@ public record struct GraphRectangle : IGraphPart start.y - end.y); if (size.x == 0 || size.y == 0) return; + Color initialColor = pen.Color; + pen.Color = Color.FromArgb((int)(opacity * 255), pen.Color); g.FillRectangle(pen.Brush, new Rectangle(start.x, end.y, size.x, size.y)); + pen.Color = initialColor; } } diff --git a/Base/Parts/GraphUiCircle.cs b/Base/Parts/GraphUiCircle.cs index 7f46411..8221a46 100644 --- a/Base/Parts/GraphUiCircle.cs +++ b/Base/Parts/GraphUiCircle.cs @@ -13,7 +13,7 @@ public record struct GraphUiCircle : IGraphPart center = new(); radius = 1; } - public GraphUiCircle(Float2 center, int radius) + public GraphUiCircle(Float2 center, int radius = 8) { this.center = center; this.radius = radius; diff --git a/Base/Parts/GraphUiText.cs b/Base/Parts/GraphUiText.cs new file mode 100644 index 0000000..420e006 --- /dev/null +++ b/Base/Parts/GraphUiText.cs @@ -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)); + } +} diff --git a/Base/Properties/PublishProfiles/FolderProfile.pubxml.user b/Base/Properties/PublishProfiles/FolderProfile.pubxml.user index 706348e..4da2d5e 100644 --- a/Base/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/Base/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - 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; + 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; \ No newline at end of file diff --git a/README.md b/README.md index ac7ed2b..dcac875 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ This is a graphing calculator I made initially for a Calculus project in a day or so. I've written a basic rendering system in Windows Forms that runs on .NET 8.0. Currently, it doesn't have a whole lot of features, but I'll be adding more in the future. Here's currently what it can do: -- Graph an equation (duh). +- Graph standard equations (duh). - There are currently some rendering issues with asymptotes which will be focused on at some point. +- Graph parametric equations. - Integrate and derive equations. - Graph a slope field of a `dy/dx =` style equation. - View a tangent line of an equation. diff --git a/Testing/Program.cs b/Testing/Program.cs index 851e7f8..35082d7 100644 --- a/Testing/Program.cs +++ b/Testing/Program.cs @@ -13,18 +13,15 @@ internal static class Program Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); - + GraphForm graph = new("One Of The Graphing Calculators Of All Time"); - Equation equ = new(Math.Sin); - SlopeField sf = new(2, (x, y) => Math.Cos(x)); - TangentLine tl = new(2, 2, equ); - graph.Graph(equ, sf, tl); - - // Now, when integrating equations, the result is much less jagged - // and much faster. Try it out! You can also select points along - // equations and such as well. Click on an equation to see for - // yourself! + Equation equA = new(Math.Sin), + equB = new(Math.Cos); + EquationDifference diff = new(2, equA, equB); + ParametricEquation equC = new(0, 20, t => 0.0375 * t * Math.Cos(t), t => 0.0625 * t * Math.Sin(t) + 3); + TangentLine tanA = new(2, 2, equA); + graph.Graph(equA, equB, diff, equC, equB.ToColumnTable(-3, 3, 2), tanA); Application.Run(graph); }