using Graphing.Extensions; using Graphing.Graphables; using Graphing.Parts; using System.Drawing.Drawing2D; using System.Text; namespace Graphing.Forms; public partial class GraphForm : Form { public Float2 ScreenCenter { get; private set; } public Float2 Dpi { get; private set; } public double ZoomLevel { get => _zoomLevel; set { double oldZoom = ZoomLevel; _zoomLevel = Math.Clamp(value, 1e-2, 1e3); int totalSegments = 0; foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count(); if (totalSegments > 10_000) { _zoomLevel = oldZoom; return; // Too many segments, stop. } } } private double _zoomLevel; private readonly Point initialWindowPos; private readonly Size initialWindowSize; public Graphable[] Graphables => ables.ToArray(); public Float2 MinVisibleGraph => ScreenSpaceToGraphSpace(new Int2(0, ClientRectangle.Height)); public Float2 MaxVisibleGraph => ScreenSpaceToGraphSpace(new Int2(ClientRectangle.Width, 0)); private readonly List ables; public GraphForm(string title) { SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.UserPaint, true); InitializeComponent(); Text = title; Graphics tempG = CreateGraphics(); Dpi = new(tempG.DpiX, tempG.DpiY); tempG.Dispose(); ables = []; ZoomLevel = 1; initialWindowPos = Location; initialWindowSize = Size; } public Int2 GraphSpaceToScreenSpace(Float2 graphPoint) { graphPoint.y = -graphPoint.y; graphPoint.x -= ScreenCenter.x; graphPoint.y -= ScreenCenter.y; graphPoint.x *= Dpi.x / ZoomLevel; graphPoint.y *= Dpi.y / ZoomLevel; graphPoint.x += ClientRectangle.Width / 2.0; graphPoint.y += ClientRectangle.Height / 2.0; return new((int)graphPoint.x, (int)graphPoint.y); } public Float2 ScreenSpaceToGraphSpace(Int2 screenPoint) { Float2 result = new(screenPoint.x, screenPoint.y); result.x -= ClientRectangle.Width / 2.0; result.y -= ClientRectangle.Height / 2.0; result.x /= Dpi.x / ZoomLevel; result.y /= Dpi.y / ZoomLevel; result.x += ScreenCenter.x; result.y += ScreenCenter.y; result.y = -result.y; return result; } protected virtual void PaintGrid(Graphics g) { double axisScale = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel))); // Draw horizontal/vertical quarter-axis. Brush quarterBrush = new SolidBrush(Color.FromArgb(unchecked((int)0xFF_E0E0E0))); Pen quarterPen = new(quarterBrush, 2); for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScale) * axisScale / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScale) * axisScale / 4; x += axisScale / 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) { Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)), endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y)); g.DrawLine(quarterPen, startPos, endPos); } // Draw horizontal/vertical semi-axis. Brush semiBrush = new SolidBrush(Color.FromArgb(unchecked((int)0xFF_999999))); Pen semiPen = new(semiBrush, 2); for (double x = Math.Ceiling(MinVisibleGraph.x / axisScale) * axisScale; x <= Math.Floor(MaxVisibleGraph.x / axisScale) * axisScale; x += axisScale) { 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) { Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)), endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y)); g.DrawLine(semiPen, startPos, endPos); } Brush mainLineBrush = new SolidBrush(Color.Black); Pen mainLinePen = new(mainLineBrush, 3); // Draw the main axis (on top of the semi axis). Int2 startCenterY = GraphSpaceToScreenSpace(new Float2(0, MinVisibleGraph.y)), endCenterY = GraphSpaceToScreenSpace(new Float2(0, MaxVisibleGraph.y)), startCenterX = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, 0)), endCenterX = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, 0)); g.DrawLine(mainLinePen, startCenterX, endCenterX); g.DrawLine(mainLinePen, startCenterY, endCenterY); } protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = SmoothingMode.HighQuality; Brush background = new SolidBrush(Color.White); g.FillRectangle(background, e.ClipRectangle); PaintGrid(g); // Draw the actual graphs. for (int i = 0; i < ables.Count; i++) { IEnumerable lines = ables[i].GetItemsToRender(this); Brush graphBrush = new SolidBrush(ables[i].Color); foreach (IGraphPart gp in lines) gp.Render(this, g, graphBrush); } base.OnPaint(e); } protected override void OnResize(EventArgs e) { base.OnResize(e); Invalidate(false); } public void Graph(Graphable able) { ables.Add(able); RegenerateMenuItems(); Invalidate(false); } private bool mouseDrag = false; private Int2 initialMouseLocation; private Float2 initialScreenCenter; protected override void OnMouseDown(MouseEventArgs e) { mouseDrag = true; initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y); initialScreenCenter = ScreenCenter; } protected override void OnMouseUp(MouseEventArgs e) { if (mouseDrag) { 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); ScreenCenter = new(initialScreenCenter.x + graphDiff.x, initialScreenCenter.y + graphDiff.y); Invalidate(false); } mouseDrag = false; } protected override void OnMouseMove(MouseEventArgs e) { if (mouseDrag) { 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); ScreenCenter = new(initialScreenCenter.x + graphDiff.x, initialScreenCenter.y + graphDiff.y); Invalidate(false); } } protected override void OnMouseWheel(MouseEventArgs e) { ZoomLevel *= 1 - e.Delta * 0.00075; // Zoom factor. Invalidate(false); } private void ResetViewportButton_Click(object? sender, EventArgs e) { ScreenCenter = new Float2(0, 0); ZoomLevel = 1; Invalidate(false); } private void GraphColorPickerButton_Click(Graphable able) { GraphColorPickerForm picker = new(this, able) { StartPosition = FormStartPosition.Manual }; picker.Location = new Point(Location.X + ClientRectangle.Width + 10, Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2); picker.ShowDialog(); RegenerateMenuItems(); } private void RegenerateMenuItems() { MenuColors.DropDownItems.Clear(); MenuEquationsDerivative.DropDownItems.Clear(); MenuEquationsIntegral.DropDownItems.Clear(); foreach (Graphable able in ables) { ToolStripMenuItem colorItem = new() { ForeColor = able.Color, Text = able.Name }; colorItem.Click += (o, e) => GraphColorPickerButton_Click(able); MenuColors.DropDownItems.Add(colorItem); if (able is Equation) { ToolStripMenuItem derivativeItem = new() { ForeColor = able.Color, Text = able.Name }; derivativeItem.Click += (o, e) => EquationComputeDerivative_Click((able as Equation)!); MenuEquationsDerivative.DropDownItems.Add(derivativeItem); ToolStripMenuItem integralItem = new() { ForeColor = able.Color, Text = able.Name }; integralItem.Click += (o, e) => EquationComputeIntegral_Click((able as Equation)!); MenuEquationsIntegral.DropDownItems.Add(integralItem); } } } private void ButtonViewportSetZoom_Click(object? sender, EventArgs e) { SetZoomForm picker = new(this) { StartPosition = FormStartPosition.Manual, }; picker.Location = new Point(Location.X + ClientRectangle.Width + 10, Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2); picker.ShowDialog(); } private void ButtonViewportSetCenter_Click(object? sender, EventArgs e) { MessageBox.Show("TODO", "Set Center Position", MessageBoxButtons.OK, MessageBoxIcon.Error); } private void ButtonViewportReset_Click(object? sender, EventArgs e) { ScreenCenter = new Float2(0, 0); ZoomLevel = 1; Invalidate(false); } private void ButtonViewportResetWindow_Click(object? sender, EventArgs e) { Location = initialWindowPos; Size = initialWindowSize; WindowState = FormWindowState.Normal; } private void EquationComputeDerivative_Click(Equation equation) { EquationDelegate equ = equation.GetDelegate(); string oldName = equation.Name, newName; if (oldName.StartsWith("Derivative of ")) newName = "Second Derivative of " + oldName[14..]; else if (oldName.StartsWith("Second Derivative of ")) newName = "Third Derivative of " + oldName[21..]; else newName = "Derivative of " + oldName; // TODO: anti-integrate (maybe). Graph(new Equation(DerivativeAtPoint(equ)) { Name = newName }); static EquationDelegate DerivativeAtPoint(EquationDelegate e) { const double step = 1e-3; return x => (e(x + step) - e(x)) / step; } } private void EquationComputeIntegral_Click(Equation equation) { EquationDelegate equ = equation.GetDelegate(); string oldName = equation.Name, newName; if (oldName.StartsWith("Integral of ")) newName = "Second Integral of " + oldName[12..]; else if (oldName.StartsWith("Second Integral of ")) newName = "Third Integral of " + oldName[19..]; else newName = "Integral of " + oldName; // TODO: anti-derive (maybe) Graph(new Equation(x => Integrate(equ, 0, x)) { Name = newName }); static double Integrate(EquationDelegate e, double lower, double upper) { // TODO: a better rendering method could make this much faster. const double step = 1e-2; double factor = 1; if (upper < lower) { factor = -1; (lower, upper) = (upper, lower); } double sum = 0; for (double x = lower; x <= upper; x += step) { sum += e(x) * step; } return sum * factor; } } private void MenuMiscCaches_Click(object? sender, EventArgs e) { // TODO: Replace with a form with a pie chart of the use by equation // and the ability to reset them. StringBuilder message = new(); long total = 0; foreach (Graphable able in ables) { long size = able.GetCacheBytes(); message.AppendLine($"{able.Name}: {size.FormatAsBytes()}"); total += size; } message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nErase cache?"); DialogResult result = MessageBox.Show(message.ToString(), "Graph Caches", MessageBoxButtons.YesNo, MessageBoxIcon.Information); if (result == DialogResult.Yes) { foreach (Graphable able in ables) able.EraseCache(); } } }