diff --git a/Base/Forms/GraphForm.cs b/Base/Forms/GraphForm.cs index 8d20d04..8f75202 100644 --- a/Base/Forms/GraphForm.cs +++ b/Base/Forms/GraphForm.cs @@ -238,19 +238,8 @@ public partial class GraphForm : Form { if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5)) { - Float2 selectedPoint = ables[i].GetSelectedPoint(this, graphMousePos); - GraphUiCircle select = new(selectedPoint); - - 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]); } } } 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 e59f358..75f2d8a 100644 --- a/Base/Graphable.cs +++ b/Base/Graphable.cs @@ -37,5 +37,5 @@ public abstract class Graphable 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 7b95593..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; @@ -52,6 +53,81 @@ public class ColumnTable : Graphable 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 a118299..e28deee 100644 --- a/Base/Graphables/Equation.cs +++ b/Base/Graphables/Equation.cs @@ -3,6 +3,7 @@ using Graphing.Forms; using Graphing.Parts; using System; using System.Collections.Generic; +using System.Drawing; namespace Graphing.Graphables; @@ -148,8 +149,15 @@ public class Equation : Graphable, IIntegrable, IDerivable, ITranslatableXY, ICo 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 index bdd341c..ebe02c7 100644 --- a/Base/Graphables/EquationDifference.cs +++ b/Base/Graphables/EquationDifference.cs @@ -1,7 +1,9 @@ using Graphing.Abstract; using Graphing.Forms; using Graphing.Parts; +using System; using System.Collections.Generic; +using System.Drawing; namespace Graphing.Graphables; @@ -42,16 +44,50 @@ public class EquationDifference : Graphable, ITranslatableX, IConvertEquation public override IEnumerable GetItemsToRender(in GraphForm graph) { Float2 pA = new(Position, points.x), - pB = new(Position, points.y), - pC = new(Position, (points.x + points.y) / 2); - return [new GraphUiText($"{points.x - points.y:0.00}", pC), - new GraphUiCircle(pA), new GraphUiCircle(pB), new GraphLine(pA, pB)]; + 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, diff --git a/Base/Graphables/IntegralEquation.cs b/Base/Graphables/IntegralEquation.cs index 8c49140..daf28d5 100644 --- a/Base/Graphables/IntegralEquation.cs +++ b/Base/Graphables/IntegralEquation.cs @@ -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/SlopeField.cs b/Base/Graphables/SlopeField.cs index d9a4549..701f506 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; @@ -101,7 +102,7 @@ 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); @@ -114,7 +115,11 @@ 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) diff --git a/Base/Graphables/TangentLine.cs b/Base/Graphables/TangentLine.cs index ab19db4..10faab3 100644 --- a/Base/Graphables/TangentLine.cs +++ b/Base/Graphables/TangentLine.cs @@ -3,6 +3,7 @@ using Graphing.Forms; using Graphing.Parts; using System; using System.Collections.Generic; +using System.Drawing; namespace Graphing.Graphables; @@ -58,7 +59,11 @@ public class TangentLine : Graphable, IConvertEquation, ITranslatableX public override IEnumerable GetItemsToRender(in GraphForm graph) { Float2 point = new(Position, currentSlope.y); - return [MakeSlopeLine(), new GraphUiCircle(point)]; + return + [ + MakeSlopeLine(), + new GraphUiCircle(point) + ]; } protected GraphLine MakeSlopeLine() { @@ -107,7 +112,7 @@ public class TangentLine : Graphable, IConvertEquation, ITranslatableX 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(); @@ -115,7 +120,15 @@ public class TangentLine : Graphable, IConvertEquation, ITranslatableX 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) diff --git a/Base/Parts/GraphRectangle.cs b/Base/Parts/GraphRectangle.cs index 161d7e2..1d4cac6 100644 --- a/Base/Parts/GraphRectangle.cs +++ b/Base/Parts/GraphRectangle.cs @@ -45,7 +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/GraphUiText.cs b/Base/Parts/GraphUiText.cs index 0d27573..420e006 100644 --- a/Base/Parts/GraphUiText.cs +++ b/Base/Parts/GraphUiText.cs @@ -9,16 +9,22 @@ public record struct GraphUiText : IGraphPart 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, bool background = true) + 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); } @@ -26,10 +32,53 @@ public record struct GraphUiText : IGraphPart public readonly void Render(in GraphForm form, in Graphics g, in Pen p) { Int2 posScreen = form.GraphSpaceToScreenSpace(position); - posScreen.y -= (int)(form.DpiFloat * font.Size * 2 / 192); + 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) { - SizeF size = g.MeasureString(text, font); g.FillRectangle(backgroundBrush!, new Rectangle(posScreen.x, posScreen.y, (int)size.Width, (int)size.Height)); } diff --git a/Testing/Program.cs b/Testing/Program.cs index ef0bc3f..35082d7 100644 --- a/Testing/Program.cs +++ b/Testing/Program.cs @@ -20,7 +20,8 @@ internal static class Program 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); - graph.Graph(equA, equB, diff, equC, equA.ToColumnTable(-1, 1, 2)); + TangentLine tanA = new(2, 2, equA); + graph.Graph(equA, equB, diff, equC, equB.ToColumnTable(-3, 3, 2), tanA); Application.Run(graph); }