diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder.sln b/AirTrajectoryBuilder/AirTrajectoryBuilder.sln new file mode 100644 index 0000000..4a72fa9 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35506.116 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AirTrajectoryBuilder", "AirTrajectoryBuilder\AirTrajectoryBuilder.csproj", "{0EA33509-A4B2-4FB3-84E5-C9773FEDF0A9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0EA33509-A4B2-4FB3-84E5-C9773FEDF0A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EA33509-A4B2-4FB3-84E5-C9773FEDF0A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EA33509-A4B2-4FB3-84E5-C9773FEDF0A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EA33509-A4B2-4FB3-84E5-C9773FEDF0A9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder.zip b/AirTrajectoryBuilder/AirTrajectoryBuilder.zip new file mode 100644 index 0000000..46350ed Binary files /dev/null and b/AirTrajectoryBuilder/AirTrajectoryBuilder.zip differ diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/AirTrajectoryBuilder.csproj b/AirTrajectoryBuilder/AirTrajectoryBuilder/AirTrajectoryBuilder.csproj new file mode 100644 index 0000000..46c1219 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/AirTrajectoryBuilder.csproj @@ -0,0 +1,15 @@ + + + + WinExe + net9.0-windows + enable + true + disable + + + + + + + \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/AirTrajectoryBuilder.csproj.user b/AirTrajectoryBuilder/AirTrajectoryBuilder/AirTrajectoryBuilder.csproj.user new file mode 100644 index 0000000..5bb867d --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/AirTrajectoryBuilder.csproj.user @@ -0,0 +1,17 @@ + + + + + Form + + + Form + + + Form + + + Form + + + \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.Designer.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.Designer.cs new file mode 100644 index 0000000..14704c8 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.Designer.cs @@ -0,0 +1,127 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace AirTrajectoryBuilder +{ + partial class MainForm + { + /// + /// 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() + { + Menu = new MenuStrip(); + MenuFile = new ToolStripMenuItem(); + MenuFileNew = new ToolStripMenuItem(); + MenuFileOpen = new ToolStripMenuItem(); + MenuRun = new ToolStripMenuItem(); + MenuRunSweep = new ToolStripMenuItem(); + FileOpener = new OpenFileDialog(); + MenuRunCancel = new ToolStripMenuItem(); + Menu.SuspendLayout(); + SuspendLayout(); + // + // Menu + // + Menu.ImageScalingSize = new Size(32, 32); + Menu.Items.AddRange(new ToolStripItem[] { MenuFile, MenuRun }); + Menu.Location = new Point(0, 0); + Menu.Name = "Menu"; + Menu.Size = new Size(1263, 42); + Menu.TabIndex = 0; + Menu.Text = "menuStrip1"; + // + // MenuFile + // + MenuFile.DropDownItems.AddRange(new ToolStripItem[] { MenuFileNew, MenuFileOpen }); + MenuFile.Name = "MenuFile"; + MenuFile.Size = new Size(71, 38); + MenuFile.Text = "File"; + // + // MenuFileNew + // + MenuFileNew.Name = "MenuFileNew"; + MenuFileNew.Size = new Size(221, 44); + MenuFileNew.Text = "New"; + MenuFileNew.Click += MenuFileNew_Click; + // + // MenuFileOpen + // + MenuFileOpen.Name = "MenuFileOpen"; + MenuFileOpen.Size = new Size(221, 44); + MenuFileOpen.Text = "Open..."; + MenuFileOpen.Click += MenuFileOpen_Click; + // + // MenuRun + // + MenuRun.DropDownItems.AddRange(new ToolStripItem[] { MenuRunSweep, MenuRunCancel }); + MenuRun.Name = "MenuRun"; + MenuRun.Size = new Size(76, 38); + MenuRun.Text = "Run"; + // + // MenuRunSweep + // + MenuRunSweep.Name = "MenuRunSweep"; + MenuRunSweep.Size = new Size(359, 44); + MenuRunSweep.Text = "Sweep..."; + MenuRunSweep.Click += MenuRunSweep_Click; + // + // FileOpener + // + FileOpener.Filter = "Scene files|*.sce|All files|*.*"; + // + // MenuRunCancel + // + MenuRunCancel.Name = "MenuRunCancel"; + MenuRunCancel.Size = new Size(359, 44); + MenuRunCancel.Text = "Cancel"; + MenuRunCancel.Click += MenuRunCancel_Click; + // + // MainForm + // + AutoScaleDimensions = new SizeF(13F, 32F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1263, 719); + Controls.Add(Menu); + MainMenuStrip = Menu; + Name = "MainForm"; + Text = "MainForm"; + Menu.ResumeLayout(false); + Menu.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private MenuStrip Menu; + private ToolStripMenuItem MenuFile; + private ToolStripMenuItem MenuFileOpen; + private ToolStripMenuItem MenuFileNew; + private OpenFileDialog FileOpener; + private ToolStripMenuItem MenuRun; + private ToolStripMenuItem MenuRunSweep; + private ToolStripMenuItem MenuRunCancel; + } +} \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.cs new file mode 100644 index 0000000..ae17a07 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.cs @@ -0,0 +1,447 @@ +using AirTrajectoryBuilder.ObjectModels; +using System.Windows.Forms; +using System.ComponentModel; +using System.Drawing; +using System; +using Nerd_STF.Mathematics; +using System.IO; +using System.Reflection; +using System.Drawing.Drawing2D; +using System.Threading.Tasks; +using System.Threading; +using AirTrajectoryBuilder.Forms; + +namespace AirTrajectoryBuilder; + +public partial class MainForm : Form +{ + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Scene Scene { get; set; } + + private readonly float scalingFactor; + private readonly string baseFolder, sceneFolder; + + public SweepParameters? SweepParameters; + + private CancellationTokenSource? simCancel; + private SimulationResult? simResult; + private SweepStatus simStatus; + private SweepCancelForm? simCancelForm; + internal SweepInfoViewer? simViewer; + + private readonly Font statusFont; + + public MainForm(Scene? initialScene) + { + InitializeComponent(); + + SetStyle(ControlStyles.UserPaint, true); + SetStyle(ControlStyles.AllPaintingInWmPaint, true); + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + + Graphics tempG = CreateGraphics(); + scalingFactor = tempG.DpiX / 96; + tempG.Dispose(); + + baseFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + sceneFolder = Path.Combine(baseFolder, "./scenes/"); + Directory.CreateDirectory(sceneFolder); + FileOpener.InitialDirectory = sceneFolder; + + Scene = initialScene ?? Scene.Default; + + statusFont = new("Segoe UI", 10); + MenuRunCancel.Enabled = false; + } + + public Int2 PlotToScreen(Float2 plot) + { + int menuHeight = Menu.Height; + int buffer = (int)(20 * scalingFactor); + + double bufferX, bufferY, maxPix, pixPerUnit; + double clientAspect = (double)(ClientRectangle.Height - menuHeight - buffer * 2) / (ClientRectangle.Width - buffer * 2), + sceneAspect = Scene.Height / Scene.Width; + + if (clientAspect > sceneAspect) + { + // Client is taller than scene, use width as max. + maxPix = ClientRectangle.Width - 2 * buffer; + pixPerUnit = maxPix / Scene.Width; + + bufferX = buffer; + bufferY = (ClientRectangle.Height + menuHeight - Scene.Height * pixPerUnit) * 0.5f; + } + else + { + // Client is wider than scene, use height as max. + maxPix = ClientRectangle.Height - menuHeight - 2 * buffer; + pixPerUnit = maxPix / Scene.Height; + + bufferX = (ClientRectangle.Width - Scene.Width * pixPerUnit) * 0.5f; + bufferY = buffer + menuHeight; + } + + return ((int)(plot.x * pixPerUnit + bufferX), + (int)((Scene.Height - plot.y) * pixPerUnit + bufferY)); + } + + protected override void OnPaint(PaintEventArgs e) + { + Graphics g = e.Graphics; + g.SmoothingMode = SmoothingMode.HighQuality; + Pen pen = new(Color.Black, scalingFactor); + SolidBrush fill = new(Color.Black); + + const int fillAlpha = 64; + + // Draw components. + foreach (ISceneObject obj in Scene.Objects) + { + if (obj is SceneRect objRect) + { + pen.Color = Color.Green; + fill.Color = Color.FromArgb(fillAlpha, pen.Color); + Int2 rectFrom = PlotToScreen(objRect.From), rectTo = PlotToScreen(objRect.To); + int minX = int.Min(rectFrom.x, rectTo.x), sizeX = MathE.Absolute(rectFrom.x - rectTo.x), + minY = int.Min(rectFrom.y, rectTo.y), sizeY = MathE.Absolute(rectFrom.y - rectTo.y); + Rectangle rect = new(new Point(minX, minY), new Size(sizeX, sizeY)); + g.FillRectangle(fill, rect); + g.DrawRectangle(pen, rect); + } + else if (obj is SceneTri objTri) + { + pen.Color = Color.Orange; + fill.Color = Color.FromArgb(fillAlpha, pen.Color); + Int2 triA = PlotToScreen(objTri.A), + triB = PlotToScreen(objTri.B), + triC = PlotToScreen(objTri.C); + g.FillPolygon(fill, [triA, triB, triC]); + g.DrawPolygon(pen, [triA, triB, triC]); + } + else if (obj is SceneEllipse objEllipse) + { + pen.Color = Color.Purple; + fill.Color = Color.FromArgb(fillAlpha, pen.Color); + Int2 min = PlotToScreen(objEllipse.Position - objEllipse.Size * 0.5), + max = PlotToScreen(objEllipse.Position + objEllipse.Size * 0.5); + Rectangle ellipseRect = new(min, max - min); + g.FillEllipse(fill, ellipseRect); + g.DrawEllipse(pen, ellipseRect); + } + } + + // Draw scene border. + pen.Color = Color.Blue; + pen.Width = scalingFactor * 2; + Int2 sceneMin = PlotToScreen((0, Scene.Height)), sceneMax = PlotToScreen((Scene.Width, 0)); + g.DrawRectangle(pen, new Rectangle(sceneMin, sceneMax - sceneMin)); + + // Draw starting position. + int startPosSize = (int)(6 * scalingFactor); + pen.Color = Color.Red; + pen.Width = scalingFactor; + fill.Color = Color.FromArgb(fillAlpha, pen.Color); + Int2 startPos = PlotToScreen(Scene.StartAt); + startPos.x -= startPosSize; + startPos.y -= startPosSize; + Rectangle startRect = new(startPos, Int2.One * startPosSize * 2); + g.FillEllipse(fill, startRect); + g.DrawEllipse(pen, startRect); + + // Draw ending position. + pen.Color = Color.Lime; + pen.Width = scalingFactor; + fill.Color = Color.FromArgb(fillAlpha, pen.Color); + Int2 endPos = PlotToScreen(Scene.EndAt); + endPos.x -= startPosSize; + endPos.y -= startPosSize; + Rectangle endRect = new(endPos, Int2.One * startPosSize * 2); + g.FillEllipse(fill, endRect); + g.DrawEllipse(pen, endRect); + + // If there's a trail, draw it. + pen.Color = Color.Red; + if (simResult is not null) + { + Point[] points = new Point[simResult.Trail.Count]; + for (int i = 0; i < points.Length; i++) + { + points[i] = PlotToScreen(simResult.Trail[i]); + } + g.DrawLines(pen, points); + + // Draw X at end point (if it's a crash and not a finish). + if (simResult.EndDistanceSquared >= SweepParameters!.Tolerance * SweepParameters.Tolerance) + { + Int2 end = points[^1]; + PointF[] xShape = [ + new Float2(-5, -5) * scalingFactor + end, + new Float2(5, 5) * scalingFactor + end, + new Float2(0, 0) * scalingFactor + end, + new Float2(-5, 5) * scalingFactor + end, + new Float2(5, -5) * scalingFactor + end, + ]; + g.DrawLines(pen, xShape); + } + } + + string message = simStatus switch + { + SweepStatus.NoSweep => "No Sweep", + SweepStatus.Sweeping => $"Sweeping... Best {simResult?.StartingConditions.StartAngle:0.000} deg, {simResult?.StartingConditions.StartVelocity:0.0} m/s", + SweepStatus.FinishedSweep => $"Done Sweeping. Best {simResult?.StartingConditions.StartAngle:0.000} deg, {simResult?.StartingConditions.StartVelocity:0.0} m/s", + SweepStatus.CancelledSweep => $"Cancelled Sweep. Best {simResult?.StartingConditions.StartAngle:0.000} deg, {simResult?.StartingConditions.StartVelocity:0.0} m/s", + _ => "???", + }; + fill.Color = Color.Blue; + SizeF size = g.MeasureString(message, statusFont); + const float spacing = 1; + g.DrawString(message, statusFont, fill, new PointF(spacing * scalingFactor, ClientRectangle.Height - size.Height - spacing * scalingFactor)); + + e.Dispose(); + } + protected override void OnClientSizeChanged(EventArgs e) + { + base.OnClientSizeChanged(e); + Invalidate(true); + } + + private void ResetSceneData() + { + simCancel?.Cancel(); + + simResult = null; + simCancel = null; + SweepParameters = null; + simStatus = SweepStatus.NoSweep; + simViewer?.Close(); + Invalidate(true); + } + + private void MenuFileNew_Click(object? sender, EventArgs e) + { + if (!TryCancelSweep()) return; + + if (!Scene.HasBeenSaved) + { + DialogResult result = MessageBox.Show( + "Are you sure you want to discard your changes?", "Lose changes?", + MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + + if (result == DialogResult.No) return; + } + Scene = Scene.Default; + ResetSceneData(); + } + private void MenuFileOpen_Click(object? sender, EventArgs e) + { + if (!TryCancelSweep()) return; + + DialogResult result; + if (!Scene.HasBeenSaved) + { + result = MessageBox.Show( + "Are you sure you want to discard your changes?", "Lose changes?", + MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + + if (result == DialogResult.No) return; + } + result = FileOpener.ShowDialog(); + if (result == DialogResult.Cancel) return; + + try + { + Scene = Scene.Read(FileOpener.FileName); + } + catch (Exception ex) + { + MessageBox.Show($"Error opening scene file: {ex.GetType().Name}"); + } + ResetSceneData(); + } + + private void MenuRunSweep_Click(object? sender, EventArgs e) + { + if (!TryCancelSweep()) return; + + SweepParametersForm param = new(this); + DialogResult result = param.ShowDialog(); + if (result == DialogResult.Cancel) return; + + SweepParameters = param.Result; + simCancel = new(); + simStatus = SweepStatus.Sweeping; + MenuRunCancel.Enabled = true; + simViewer ??= new SweepInfoViewer(this); + simViewer.UncompleteSweep(); + simViewer.Show(); + Task.Run(() => SweepSimulation(SweepParameters, simCancel.Token)); + } + + public bool TryCancelSweep() + { + if (simStatus == SweepStatus.Sweeping && simCancel is not null) + { + SweepCancelForm form = new(); + simCancelForm = form; + DialogResult result = form.ShowDialog(); + if (result == DialogResult.No) return false; + + simCancel?.Cancel(); + return true; + } + return true; + } + + private void SweepSimulation(SweepParameters param, CancellationToken token) + { + Float2 diff = param.Scene.EndAt - param.Scene.StartAt; + double minAngle = Math.Atan2(diff.y, diff.x) * Constants.Pi / 180, + maxAngle = Constants.Pi / 2, + angleStep = param.AngleDelta * Constants.Pi / 180; + + double closest = double.MaxValue, tolSquared = param.Tolerance * param.Tolerance; + bool end = false; + int angleSteps = (int)((maxAngle - minAngle) / angleStep); + int steps = 0; + simViewer?.SetMaxIters(angleSteps); + for (double ang = minAngle; ang <= maxAngle; ang += angleStep) + { + for (double vel = param.SpeedMin; vel <= param.SpeedMax; vel += param.SpeedDelta) + { + if (token.IsCancellationRequested) + { + end = true; + break; + } + SimulationParameters simParams = new() + { + DeltaTime = param.TimeDelta, + Gravity = param.Gravity, + StartAngle = ang, + StartVelocity = vel, + Scene = Scene, + ToleranceSquared = tolSquared, + ObjectRadius = param.ObjectRadius, + DragCoefficient = param.DragCoefficient, + Mass = param.Mass, + AirDensity = param.AirDensity, + GenerateTable = param.FileMode != ResultsFileMode.None + }; + SimulationResult result = SimulateTrajectory(simParams, token); + if (result.EndDistanceSquared < closest) + { + Invoke(() => + { + simResult = result; + }); + Invalidate(true); + closest = result.EndDistanceSquared; + } + } + if (end) break; + simViewer?.SetCurrentIters(steps); + steps++; + } + simCancel = null; + simStatus = token.IsCancellationRequested ? SweepStatus.CancelledSweep : SweepStatus.FinishedSweep; + if (simCancelForm is not null) + { + simCancelForm.DialogResult = DialogResult.No; + simCancelForm.Close(); + } + MenuRunCancel.Enabled = false; + simViewer?.Invoke(() => simViewer?.CompleteSweep(simResult!)); + Invalidate(true); + } + private static SimulationResult SimulateTrajectory(SimulationParameters param, CancellationToken token) + { + Float2 pos = param.Scene.StartAt; + Float2 vel = (Math.Cos(param.StartAngle) * param.StartVelocity, + Math.Sin(param.StartAngle) * param.StartVelocity); + Float2 gravity = (0, param.Gravity); + double halfArea = 0.5 * param.ObjectRadius * param.ObjectRadius * Constants.Pi; + + SimulationResult result = new(param); + double trailPointsPerSecond = 5; + int ticksPerTrailPoint = (int)(1 / (trailPointsPerSecond * param.DeltaTime)); + + if (param.GenerateTable) result.Table = []; + + int ticks = 0; + double time = 0; + while (true) + { + Float2 air = (halfArea * param.DragCoefficient * param.AirDensity * vel.x * vel.x, + halfArea * param.DragCoefficient * param.AirDensity * vel.y * vel.y); + + Float2 acc = gravity; + if (param.Mass > 0) + { + air /= param.Mass; + acc.x -= air.x * Math.Sign(vel.x); + acc.y -= air.y * Math.Sign(vel.y); + } + + if (param.GenerateTable) result.Table!.Add(new(pos, vel, acc)); + + if (pos.x < 0 || pos.x >= param.Scene.Width || + pos.y < 0 || pos.y >= param.Scene.Height) + { + result.Trail.Add(pos); + break; + } + + if (token.IsCancellationRequested) break; + + bool collide = false; + for (int i = 0; i < param.Scene.Objects.Count; i++) + { + if (param.Scene.Objects[i].Contains(pos)) + { + collide = true; + break; + } + } + if (collide) + { + result.Trail.Add(pos); + break; + } + + Float2 diff = param.Scene.EndAt - pos; + result.EndDistanceSquared = diff.x * diff.x + diff.y * diff.y; + if (result.EndDistanceSquared <= param.ToleranceSquared) + { + result.Trail.Add(pos); + break; + } + + if (ticks % ticksPerTrailPoint == 0) result.Trail.Add(pos); + + pos += vel * param.DeltaTime; + vel += acc * param.DeltaTime; + + time += param.DeltaTime; + } + result.StartingConditions.StartAngle *= 180 / Constants.Pi; + result.EndSpeed = vel.Magnitude; + result.Duration = time; + + return result; + } + + private void MenuRunCancel_Click(object sender, EventArgs e) + { + simCancel?.Cancel(); + } + + public enum SweepStatus + { + NoSweep, + Sweeping, + FinishedSweep, + CancelledSweep + } +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.resx b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.resx new file mode 100644 index 0000000..bafbbf5 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/MainForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 17, 17 + + + 157, 17 + + \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.Designer.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.Designer.cs new file mode 100644 index 0000000..fecf1d0 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.Designer.cs @@ -0,0 +1,87 @@ +namespace AirTrajectoryBuilder.Forms +{ + partial class SweepCancelForm + { + /// + /// 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() + { + MessageLabel = new System.Windows.Forms.Label(); + YesButton = new System.Windows.Forms.Button(); + button2 = new System.Windows.Forms.Button(); + SuspendLayout(); + // + // MessageLabel + // + MessageLabel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + MessageLabel.Location = new System.Drawing.Point(12, 12); + MessageLabel.Name = "MessageLabel"; + MessageLabel.Size = new System.Drawing.Size(637, 135); + MessageLabel.TabIndex = 0; + MessageLabel.Text = "Are you sure you want to cancel the sweep?"; + MessageLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // YesButton + // + YesButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + YesButton.DialogResult = System.Windows.Forms.DialogResult.Yes; + YesButton.Location = new System.Drawing.Point(178, 150); + YesButton.Name = "YesButton"; + YesButton.Size = new System.Drawing.Size(150, 46); + YesButton.TabIndex = 1; + YesButton.Text = "Yes"; + YesButton.UseVisualStyleBackColor = true; + // + // button2 + // + button2.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; + button2.DialogResult = System.Windows.Forms.DialogResult.No; + button2.Location = new System.Drawing.Point(334, 150); + button2.Name = "button2"; + button2.Size = new System.Drawing.Size(150, 46); + button2.TabIndex = 2; + button2.Text = "No"; + button2.UseVisualStyleBackColor = true; + // + // SweepCancelForm + // + AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(661, 208); + Controls.Add(button2); + Controls.Add(YesButton); + Controls.Add(MessageLabel); + Name = "SweepCancelForm"; + Text = "Cancel the Sweep?"; + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.Label MessageLabel; + private System.Windows.Forms.Button YesButton; + private System.Windows.Forms.Button button2; + } +} \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.cs new file mode 100644 index 0000000..e4bf1ae --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.cs @@ -0,0 +1,11 @@ +using System.Windows.Forms; + +namespace AirTrajectoryBuilder.Forms; + +public partial class SweepCancelForm : Form +{ + public SweepCancelForm() + { + InitializeComponent(); + } +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.resx b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepCancelForm.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/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.Designer.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.Designer.cs new file mode 100644 index 0000000..a48fa93 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.Designer.cs @@ -0,0 +1,99 @@ +namespace AirTrajectoryBuilder.Forms +{ + partial class SweepInfoViewer + { + /// + /// 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() + { + SweepProgress = new System.Windows.Forms.ProgressBar(); + ProgressLabel = new System.Windows.Forms.Label(); + ResultsPanel = new System.Windows.Forms.Panel(); + ResultsLabel = new System.Windows.Forms.Label(); + ResultsPanel.SuspendLayout(); + SuspendLayout(); + // + // SweepProgress + // + SweepProgress.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + SweepProgress.Location = new System.Drawing.Point(12, 229); + SweepProgress.Name = "SweepProgress"; + SweepProgress.Size = new System.Drawing.Size(673, 20); + SweepProgress.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + SweepProgress.TabIndex = 0; + SweepProgress.Value = 50; + // + // ProgressLabel + // + ProgressLabel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + ProgressLabel.Location = new System.Drawing.Point(12, 194); + ProgressLabel.Name = "ProgressLabel"; + ProgressLabel.Size = new System.Drawing.Size(673, 32); + ProgressLabel.TabIndex = 1; + ProgressLabel.Text = "Conducting Sweep..."; + ProgressLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // ResultsPanel + // + ResultsPanel.AutoScroll = true; + ResultsPanel.Controls.Add(ResultsLabel); + ResultsPanel.Dock = System.Windows.Forms.DockStyle.Fill; + ResultsPanel.Location = new System.Drawing.Point(0, 0); + ResultsPanel.Name = "ResultsPanel"; + ResultsPanel.Size = new System.Drawing.Size(697, 405); + ResultsPanel.TabIndex = 2; + ResultsPanel.Visible = false; + // + // ResultsLabel + // + ResultsLabel.Location = new System.Drawing.Point(12, 9); + ResultsLabel.Name = "ResultsLabel"; + ResultsLabel.Size = new System.Drawing.Size(682, 387); + ResultsLabel.TabIndex = 0; + ResultsLabel.Text = "label1"; + // + // SweepInfoViewer + // + AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(697, 405); + Controls.Add(ResultsPanel); + Controls.Add(ProgressLabel); + Controls.Add(SweepProgress); + Name = "SweepInfoViewer"; + StartPosition = System.Windows.Forms.FormStartPosition.Manual; + Text = "Simulation Results"; + ResultsPanel.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.ProgressBar SweepProgress; + private System.Windows.Forms.Label ProgressLabel; + private System.Windows.Forms.Panel ResultsPanel; + private System.Windows.Forms.Label ResultsLabel; + } +} \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.cs new file mode 100644 index 0000000..e6c3854 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.cs @@ -0,0 +1,97 @@ +using AirTrajectoryBuilder.ObjectModels; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace AirTrajectoryBuilder.Forms; + +public partial class SweepInfoViewer : Form +{ + private readonly MainForm mainForm; + private SimulationResult? results; + + public SweepInfoViewer(MainForm mainForm) + { + InitializeComponent(); + ResultsPanel.Visible = false; + this.mainForm = mainForm; + + Location = + new Point(mainForm.Location.X + mainForm.Size.Width, + mainForm.Location.Y + (mainForm.Size.Height - Size.Height) / 2); + } + + protected override void OnClosing(CancelEventArgs e) + { + if (!mainForm.TryCancelSweep()) + { + e.Cancel = true; + return; + } + base.OnClosing(e); + mainForm.simViewer = null; + } + + public void SetMaxIters(int max) + { + SweepProgress.Maximum = max; + } + public void SetCurrentIters(int val) + { + SweepProgress.Value = val; + } + + public void CompleteSweep(SimulationResult best) + { + results = best; + + SweepProgress.Visible = false; + ProgressLabel.Visible = false; + + DisplayResults(); + } + public void UncompleteSweep() + { + SweepProgress.Visible = true; + ProgressLabel.Visible = true; + ResultsPanel.Visible = false; + Invalidate(true); + } + + private void SetLabelSize() + { + ResultsLabel.Location = new Point(0, 0); + ResultsLabel.Size = new(ResultsPanel.ClientRectangle.Width, + ResultsLabel.PreferredHeight); + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + SetLabelSize(); + } + + private void DisplayResults() + { + ResultsPanel.Visible = true; + if (results is null) return; + + ResultsLabel.Text = $""" + + Initial Angle: {results.StartingConditions.StartAngle:0.00} degrees + Initial Velocity: {results.StartingConditions.StartVelocity:0.0} m/s + Gravity: {results.StartingConditions.Gravity:0.000} m/s^2 + Delta Time: {results.StartingConditions.DeltaTime:0.000} seconds + + Final Velocity: {results.EndSpeed:0.0} m/s + + Duration: {results.Duration:0.00} seconds + + Error: off by {results.EndDistanceSquared:0.000} meters + + """; + SetLabelSize(); + SetLabelSize(); // ...nice. But required! + } +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.resx b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepInfoViewer.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/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.Designer.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.Designer.cs new file mode 100644 index 0000000..8d047f7 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.Designer.cs @@ -0,0 +1,361 @@ +namespace AirTrajectoryBuilder +{ + partial class SweepParametersForm + { + /// + /// 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() + { + AngleSweepValue = new System.Windows.Forms.TextBox(); + AngleSweepLabel = new System.Windows.Forms.Label(); + SpeedMinLabel = new System.Windows.Forms.Label(); + SpeedMinValue = new System.Windows.Forms.TextBox(); + SpeedMaxLabel = new System.Windows.Forms.Label(); + SpeedDeltaLabel = new System.Windows.Forms.Label(); + SpeedMaxValue = new System.Windows.Forms.TextBox(); + SpeedDeltaValue = new System.Windows.Forms.TextBox(); + RunButton = new System.Windows.Forms.Button(); + GravityLabel = new System.Windows.Forms.Label(); + GravityValue = new System.Windows.Forms.TextBox(); + TimeDeltaLabel = new System.Windows.Forms.Label(); + TimeDeltaValue = new System.Windows.Forms.TextBox(); + CancelButton = new System.Windows.Forms.Button(); + ProjectileMotionLabel = new System.Windows.Forms.Label(); + AirTrajectoryLabel = new System.Windows.Forms.Label(); + ObjectRadiusValue = new System.Windows.Forms.TextBox(); + ObjectRadiusLabel = new System.Windows.Forms.Label(); + MassValue = new System.Windows.Forms.TextBox(); + MassLabel = new System.Windows.Forms.Label(); + AirDensityValue = new System.Windows.Forms.TextBox(); + AirDensityLabel = new System.Windows.Forms.Label(); + DragCoefficientValue = new System.Windows.Forms.TextBox(); + DragCoefficientLabel = new System.Windows.Forms.Label(); + ResultsLabel = new System.Windows.Forms.Label(); + FileOutputLabel = new System.Windows.Forms.Label(); + FileOutputValue = new System.Windows.Forms.ComboBox(); + SuspendLayout(); + // + // AngleSweepValue + // + AngleSweepValue.Location = new System.Drawing.Point(312, 122); + AngleSweepValue.Name = "AngleSweepValue"; + AngleSweepValue.Size = new System.Drawing.Size(101, 39); + AngleSweepValue.TabIndex = 0; + // + // AngleSweepLabel + // + AngleSweepLabel.Location = new System.Drawing.Point(82, 122); + AngleSweepLabel.Name = "AngleSweepLabel"; + AngleSweepLabel.Size = new System.Drawing.Size(224, 39); + AngleSweepLabel.TabIndex = 1; + AngleSweepLabel.Text = "Angle Sweep Delta"; + AngleSweepLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // SpeedMinLabel + // + SpeedMinLabel.Location = new System.Drawing.Point(12, 168); + SpeedMinLabel.Name = "SpeedMinLabel"; + SpeedMinLabel.Size = new System.Drawing.Size(159, 39); + SpeedMinLabel.TabIndex = 2; + SpeedMinLabel.Text = "Speed Min"; + SpeedMinLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // SpeedMinValue + // + SpeedMinValue.Location = new System.Drawing.Point(36, 210); + SpeedMinValue.Name = "SpeedMinValue"; + SpeedMinValue.Size = new System.Drawing.Size(101, 39); + SpeedMinValue.TabIndex = 3; + // + // SpeedMaxLabel + // + SpeedMaxLabel.Location = new System.Drawing.Point(177, 168); + SpeedMaxLabel.Name = "SpeedMaxLabel"; + SpeedMaxLabel.Size = new System.Drawing.Size(159, 39); + SpeedMaxLabel.TabIndex = 4; + SpeedMaxLabel.Text = "Speed Max"; + SpeedMaxLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // SpeedDeltaLabel + // + SpeedDeltaLabel.Location = new System.Drawing.Point(342, 168); + SpeedDeltaLabel.Name = "SpeedDeltaLabel"; + SpeedDeltaLabel.Size = new System.Drawing.Size(159, 39); + SpeedDeltaLabel.TabIndex = 5; + SpeedDeltaLabel.Text = "Sweep Delta"; + SpeedDeltaLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // SpeedMaxValue + // + SpeedMaxValue.Location = new System.Drawing.Point(204, 210); + SpeedMaxValue.Name = "SpeedMaxValue"; + SpeedMaxValue.Size = new System.Drawing.Size(101, 39); + SpeedMaxValue.TabIndex = 6; + // + // SpeedDeltaValue + // + SpeedDeltaValue.Location = new System.Drawing.Point(371, 210); + SpeedDeltaValue.Name = "SpeedDeltaValue"; + SpeedDeltaValue.Size = new System.Drawing.Size(101, 39); + SpeedDeltaValue.TabIndex = 7; + // + // RunButton + // + RunButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + RunButton.DialogResult = System.Windows.Forms.DialogResult.OK; + RunButton.Location = new System.Drawing.Point(198, 707); + RunButton.Name = "RunButton"; + RunButton.Size = new System.Drawing.Size(150, 46); + RunButton.TabIndex = 8; + RunButton.Text = "Sweep"; + RunButton.UseVisualStyleBackColor = true; + // + // GravityLabel + // + GravityLabel.Location = new System.Drawing.Point(138, 258); + GravityLabel.Name = "GravityLabel"; + GravityLabel.Size = new System.Drawing.Size(113, 39); + GravityLabel.TabIndex = 10; + GravityLabel.Text = "Gravity"; + GravityLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // GravityValue + // + GravityValue.Location = new System.Drawing.Point(257, 258); + GravityValue.Name = "GravityValue"; + GravityValue.Size = new System.Drawing.Size(101, 39); + GravityValue.TabIndex = 9; + // + // TimeDeltaLabel + // + TimeDeltaLabel.Location = new System.Drawing.Point(131, 75); + TimeDeltaLabel.Name = "TimeDeltaLabel"; + TimeDeltaLabel.Size = new System.Drawing.Size(137, 39); + TimeDeltaLabel.TabIndex = 12; + TimeDeltaLabel.Text = "Time Delta"; + TimeDeltaLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // TimeDeltaValue + // + TimeDeltaValue.Location = new System.Drawing.Point(274, 75); + TimeDeltaValue.Name = "TimeDeltaValue"; + TimeDeltaValue.Size = new System.Drawing.Size(101, 39); + TimeDeltaValue.TabIndex = 11; + // + // CancelButton + // + CancelButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + CancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + CancelButton.Location = new System.Drawing.Point(354, 707); + CancelButton.Name = "CancelButton"; + CancelButton.Size = new System.Drawing.Size(150, 46); + CancelButton.TabIndex = 13; + CancelButton.Text = "Cancel"; + CancelButton.UseVisualStyleBackColor = true; + // + // ProjectileMotionLabel + // + ProjectileMotionLabel.Font = new System.Drawing.Font("Segoe UI", 10.125F, System.Drawing.FontStyle.Bold); + ProjectileMotionLabel.Location = new System.Drawing.Point(12, 9); + ProjectileMotionLabel.Name = "ProjectileMotionLabel"; + ProjectileMotionLabel.Size = new System.Drawing.Size(489, 39); + ProjectileMotionLabel.TabIndex = 14; + ProjectileMotionLabel.Text = "General Parameters:"; + ProjectileMotionLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // AirTrajectoryLabel + // + AirTrajectoryLabel.Font = new System.Drawing.Font("Segoe UI", 10.125F, System.Drawing.FontStyle.Bold); + AirTrajectoryLabel.Location = new System.Drawing.Point(14, 327); + AirTrajectoryLabel.Name = "AirTrajectoryLabel"; + AirTrajectoryLabel.Size = new System.Drawing.Size(489, 39); + AirTrajectoryLabel.TabIndex = 15; + AirTrajectoryLabel.Text = "Aerodynamics (Optional):"; + AirTrajectoryLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // ObjectRadiusValue + // + ObjectRadiusValue.Location = new System.Drawing.Point(82, 419); + ObjectRadiusValue.Name = "ObjectRadiusValue"; + ObjectRadiusValue.Size = new System.Drawing.Size(101, 39); + ObjectRadiusValue.TabIndex = 17; + // + // ObjectRadiusLabel + // + ObjectRadiusLabel.Location = new System.Drawing.Point(12, 377); + ObjectRadiusLabel.Name = "ObjectRadiusLabel"; + ObjectRadiusLabel.Size = new System.Drawing.Size(239, 39); + ObjectRadiusLabel.TabIndex = 16; + ObjectRadiusLabel.Text = "Object Radius"; + ObjectRadiusLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // MassValue + // + MassValue.Location = new System.Drawing.Point(335, 419); + MassValue.Name = "MassValue"; + MassValue.Size = new System.Drawing.Size(101, 39); + MassValue.TabIndex = 19; + // + // MassLabel + // + MassLabel.Location = new System.Drawing.Point(265, 377); + MassLabel.Name = "MassLabel"; + MassLabel.Size = new System.Drawing.Size(239, 39); + MassLabel.TabIndex = 18; + MassLabel.Text = "Object Mass"; + MassLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // AirDensityValue + // + AirDensityValue.Location = new System.Drawing.Point(335, 512); + AirDensityValue.Name = "AirDensityValue"; + AirDensityValue.Size = new System.Drawing.Size(101, 39); + AirDensityValue.TabIndex = 23; + // + // AirDensityLabel + // + AirDensityLabel.Location = new System.Drawing.Point(265, 470); + AirDensityLabel.Name = "AirDensityLabel"; + AirDensityLabel.Size = new System.Drawing.Size(239, 39); + AirDensityLabel.TabIndex = 22; + AirDensityLabel.Text = "Air Density"; + AirDensityLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // DragCoefficientValue + // + DragCoefficientValue.Location = new System.Drawing.Point(82, 512); + DragCoefficientValue.Name = "DragCoefficientValue"; + DragCoefficientValue.Size = new System.Drawing.Size(101, 39); + DragCoefficientValue.TabIndex = 21; + // + // DragCoefficientLabel + // + DragCoefficientLabel.Location = new System.Drawing.Point(12, 470); + DragCoefficientLabel.Name = "DragCoefficientLabel"; + DragCoefficientLabel.Size = new System.Drawing.Size(239, 39); + DragCoefficientLabel.TabIndex = 20; + DragCoefficientLabel.Text = "Drag Coefficient"; + DragCoefficientLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // ResultsLabel + // + ResultsLabel.Font = new System.Drawing.Font("Segoe UI", 10.125F, System.Drawing.FontStyle.Bold); + ResultsLabel.Location = new System.Drawing.Point(14, 583); + ResultsLabel.Name = "ResultsLabel"; + ResultsLabel.Size = new System.Drawing.Size(489, 39); + ResultsLabel.TabIndex = 24; + ResultsLabel.Text = "Results:"; + ResultsLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // FileOutputLabel + // + FileOutputLabel.Location = new System.Drawing.Point(37, 622); + FileOutputLabel.Name = "FileOutputLabel"; + FileOutputLabel.Size = new System.Drawing.Size(156, 64); + FileOutputLabel.TabIndex = 25; + FileOutputLabel.Text = "File Output"; + FileOutputLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // FileOutputValue + // + FileOutputValue.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + FileOutputValue.FormattingEnabled = true; + FileOutputValue.Items.AddRange(new object[] { "None", "Text - Best", "Text - All", "Json - Best", "Json - All", "Binary - Best", "Binary - All" }); + FileOutputValue.Location = new System.Drawing.Point(199, 635); + FileOutputValue.MaxLength = 1; + FileOutputValue.Name = "FileOutputValue"; + FileOutputValue.Size = new System.Drawing.Size(242, 40); + FileOutputValue.TabIndex = 26; + // + // SweepParametersForm + // + AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(516, 765); + Controls.Add(FileOutputValue); + Controls.Add(FileOutputLabel); + Controls.Add(ResultsLabel); + Controls.Add(AirDensityValue); + Controls.Add(AirDensityLabel); + Controls.Add(DragCoefficientValue); + Controls.Add(DragCoefficientLabel); + Controls.Add(MassValue); + Controls.Add(MassLabel); + Controls.Add(ObjectRadiusValue); + Controls.Add(ObjectRadiusLabel); + Controls.Add(AirTrajectoryLabel); + Controls.Add(ProjectileMotionLabel); + Controls.Add(CancelButton); + Controls.Add(TimeDeltaLabel); + Controls.Add(TimeDeltaValue); + Controls.Add(GravityLabel); + Controls.Add(GravityValue); + Controls.Add(RunButton); + Controls.Add(SpeedDeltaValue); + Controls.Add(SpeedMaxValue); + Controls.Add(SpeedDeltaLabel); + Controls.Add(SpeedMaxLabel); + Controls.Add(SpeedMinValue); + Controls.Add(SpeedMinLabel); + Controls.Add(AngleSweepLabel); + Controls.Add(AngleSweepValue); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + Name = "SweepParametersForm"; + Text = "Set Sweep Parameters"; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private System.Windows.Forms.TextBox AngleSweepValue; + private System.Windows.Forms.Label AngleSweepLabel; + private System.Windows.Forms.Label SpeedMinLabel; + private System.Windows.Forms.TextBox SpeedMinValue; + private System.Windows.Forms.Label SpeedMaxLabel; + private System.Windows.Forms.Label SpeedDeltaLabel; + private System.Windows.Forms.TextBox SpeedMaxValue; + private System.Windows.Forms.TextBox SpeedDeltaValue; + private System.Windows.Forms.Button RunButton; + private System.Windows.Forms.Label GravityLabel; + private System.Windows.Forms.TextBox GravityValue; + private System.Windows.Forms.Label TimeDeltaLabel; + private System.Windows.Forms.TextBox TimeDeltaValue; + private System.Windows.Forms.Button CancelButton; + private System.Windows.Forms.Label ProjectileMotionLabel; + private System.Windows.Forms.Label AirTrajectoryLabel; + private System.Windows.Forms.TextBox ObjectRadiusValue; + private System.Windows.Forms.Label ObjectRadiusLabel; + private System.Windows.Forms.TextBox MassValue; + private System.Windows.Forms.Label MassLabel; + private System.Windows.Forms.TextBox AirDensityValue; + private System.Windows.Forms.Label AirDensityLabel; + private System.Windows.Forms.TextBox DragCoefficientValue; + private System.Windows.Forms.Label DragCoefficientLabel; + private System.Windows.Forms.Label ResultsLabel; + private System.Windows.Forms.Label FileOutputLabel; + private System.Windows.Forms.ComboBox FileOutputValue; + } +} \ No newline at end of file diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.cs new file mode 100644 index 0000000..6defeee --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.cs @@ -0,0 +1,156 @@ +using AirTrajectoryBuilder.ObjectModels; +using System.Windows.Forms; +using System.ComponentModel; +using System.IO; +using System; + +namespace AirTrajectoryBuilder; + +public partial class SweepParametersForm : Form +{ + private static readonly FileMode[] fileModeValues = Enum.GetValues(); + + public double AngleDelta = 0.1, + SpeedDelta = 0.5, + SpeedMin = 10, + SpeedMax = 100, + Gravity = -9.81, + TimeDelta = 0.01; + + public double ObjectRadius = 0, + DragCoefficient = 0, + Mass = 0, + AirDensity = 1.225; + + public ResultsFileMode FileMode = ResultsFileMode.None; + + private static SweepParameters? previous = null; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SweepParameters Result + { + get + { + SweepParameters result = new() + { + AngleDelta = AngleDelta, + SpeedDelta = SpeedDelta, + SpeedMin = SpeedMin, + SpeedMax = SpeedMax, + Gravity = Gravity, + TimeDelta = TimeDelta, + ObjectRadius = ObjectRadius, + DragCoefficient = DragCoefficient, + Mass = Mass, + AirDensity = AirDensity, + Tolerance = 1e-1, // TODO + Scene = form.Scene, + FileMode = FileMode + }; + previous = result; + return result; + } + set + { + AngleDelta = value.AngleDelta; + SpeedDelta = value.SpeedDelta; + SpeedMin = value.SpeedMin; + SpeedMax = value.SpeedMax; + Gravity = value.Gravity; + TimeDelta = value.TimeDelta; + ObjectRadius = value.ObjectRadius; + DragCoefficient = value.DragCoefficient; + Mass = value.Mass; + AirDensity = value.AirDensity; + FileMode = value.FileMode; + + previous = value; + SetValues(); + } + } + private readonly MainForm form; + + public SweepParametersForm(MainForm form) + { + InitializeComponent(); + this.form = form; + + AngleSweepValue.Leave += (o, e) => + { + if (!double.TryParse(AngleSweepValue.Text, out AngleDelta)) + AngleSweepValue.Text = AngleDelta.ToString(); + }; + SpeedDeltaValue.Leave += (o, e) => + { + if (!double.TryParse(SpeedDeltaValue.Text, out SpeedDelta)) + SpeedDeltaValue.Text = SpeedDelta.ToString(); + }; + SpeedMinValue.Leave += (o, e) => + { + if (!double.TryParse(SpeedMinValue.Text, out SpeedMin)) + SpeedMinValue.Text = SpeedMin.ToString(); + }; + SpeedMaxValue.Leave += (o, e) => + { + if (!double.TryParse(SpeedMaxValue.Text, out SpeedMax)) + SpeedMaxValue.Text = SpeedMax.ToString(); + }; + GravityValue.Leave += (o, e) => + { + if (!double.TryParse(GravityValue.Text, out Gravity)) + GravityValue.Text = Gravity.ToString(); + }; + TimeDeltaValue.Leave += (o, e) => + { + if (!double.TryParse(TimeDeltaValue.Text, out TimeDelta)) + TimeDeltaValue.Text = TimeDelta.ToString(); + }; + ObjectRadiusValue.Leave += (o, e) => + { + if (!double.TryParse(ObjectRadiusValue.Text, out ObjectRadius)) + ObjectRadiusValue.Text = ObjectRadius.ToString(); + }; + MassValue.Leave += (o, e) => + { + if (!double.TryParse(MassValue.Text, out Mass)) + MassValue.Text = Mass.ToString(); + }; + DragCoefficientValue.Leave += (o, e) => + { + if (!double.TryParse(DragCoefficientValue.Text, out DragCoefficient)) + DragCoefficientValue.Text = DragCoefficient.ToString(); + }; + AirDensityValue.Leave += (o, e) => + { + if (!double.TryParse(AirDensityValue.Text, out AirDensity)) + AirDensityValue.Text = AirDensity.ToString(); + }; + FileOutputValue.Leave += (o, e) => + { + int index = FileOutputValue.SelectedIndex; + if (index < 0 || index >= FileOutputValue.Items.Count) + { + FileOutputValue.SelectedIndex = (int)FileMode; + } + else FileMode = (ResultsFileMode)FileOutputValue.SelectedIndex; + }; + + if (previous is null) SetValues(); + else Result = previous; + } + + private void SetValues() + { + AngleSweepValue.Text = AngleDelta.ToString(); + SpeedDeltaValue.Text = SpeedDelta.ToString(); + SpeedMinValue.Text = SpeedMin.ToString(); + SpeedMaxValue.Text = SpeedMax.ToString(); + GravityValue.Text = Gravity.ToString(); + TimeDeltaValue.Text = TimeDelta.ToString(); + ObjectRadiusValue.Text = ObjectRadius.ToString(); + MassValue.Text = Mass.ToString(); + DragCoefficientValue.Text = DragCoefficient.ToString(); + AirDensityValue.Text = AirDensity.ToString(); + FileOutputValue.SelectedIndex = (int)FileMode; + } +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.resx b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Forms/SweepParametersForm.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/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/ISceneObject.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/ISceneObject.cs new file mode 100644 index 0000000..91abb38 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/ISceneObject.cs @@ -0,0 +1,9 @@ +using Nerd_STF.Mathematics; + +namespace AirTrajectoryBuilder.ObjectModels; + +public interface ISceneObject +{ + public bool Contains(Float2 point); + public ISceneObject DeepCopy(); +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/ResultsFileMode.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/ResultsFileMode.cs new file mode 100644 index 0000000..0d17711 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/ResultsFileMode.cs @@ -0,0 +1,12 @@ +namespace AirTrajectoryBuilder.ObjectModels; + +public enum ResultsFileMode +{ + None, + TextBest, + TextAll, + JsonBest, + JsonAll, + BinaryBest, + BinaryAll +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/Scene.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/Scene.cs new file mode 100644 index 0000000..3d0e620 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/Scene.cs @@ -0,0 +1,146 @@ +using Nerd_STF.Mathematics; +using System.Collections.Generic; +using System.IO; + +namespace AirTrajectoryBuilder.ObjectModels +{ + public class Scene + { + public static Scene Default => new() + { + Width = 100, + Height = 60, + Objects = [], + HasBeenSaved = true, + FilePath = null, + EndAt = (100, 0) + }; + + public double Width { get; set; } + public double Height { get; set; } + + public Float2 StartAt { get; set; } + public Float2 EndAt { get; set; } + + public bool HasBeenSaved { get; set; } + public string? FilePath { get; set; } + + public List Objects { get; private set; } = []; + + public Scene DeepCopy() + { + Scene copy = new() + { + Width = Width, + Height = Height, + StartAt = StartAt, + EndAt = EndAt, + HasBeenSaved = HasBeenSaved, + FilePath = FilePath, + Objects = [] + }; + foreach (ISceneObject obj in Objects) copy.Objects.Add(obj.DeepCopy()); + return copy; + } + + public static Scene Read(string path) + { + if (!File.Exists(path)) throw new IOException(); + StreamReader reader = new(path); + + string? line; + Scene? scene = null; + while ((line = reader.ReadLine()) is not null) + { + if (string.IsNullOrWhiteSpace(line)) continue; + string[] parts = line.Split(' '); + + switch (parts[0]) + { + case "Scene": + if (parts.Length != 4) throw new IOException(); + else if (parts[2] != "by") throw new IOException(); + scene = new() + { + Width = double.Parse(parts[1]), + Height = double.Parse(parts[3]), + Objects = [], + HasBeenSaved = true, + FilePath = path, + }; + break; + case "Rect": + if (scene is null) throw new IOException(); + else if (parts.Length != 6) throw new IOException(); + else if (parts[3] != "to") throw new IOException(); + SceneRect rect = new() + { + From = (double.Parse(parts[1]), double.Parse(parts[2])), + To = (double.Parse(parts[4]), double.Parse(parts[5])) + }; + scene.Objects.Add(rect); + break; + case "Tri": + if (scene is null) throw new IOException(); + else if (parts.Length != 9) throw new IOException(); + else if (parts[3] != "and" || parts[6] != "and") throw new IOException(); + SceneTri tri = new() + { + A = (double.Parse(parts[1]), double.Parse(parts[2])), + B = (double.Parse(parts[4]), double.Parse(parts[5])), + C = (double.Parse(parts[7]), double.Parse(parts[8])) + }; + scene.Objects.Add(tri); + break; + case "Ellipse": + if (scene is null) throw new IOException(); + else if (parts.Length != 7) throw new IOException(); + else if (parts[3] != "and" || parts[5] != "by") throw new IOException(); + SceneEllipse ellipse = new() + { + Position = (double.Parse(parts[1]), double.Parse(parts[2])), + Size = (double.Parse(parts[4]), double.Parse(parts[6])) + }; + scene.Objects.Add(ellipse); + break; + case "Start": + if (scene is null) throw new IOException(); + else if (parts.Length != 3) throw new IOException(); + scene.StartAt = (double.Parse(parts[1]), double.Parse(parts[2])); + break; + case "End": + if (scene is null) throw new IOException(); + else if (parts.Length != 3) throw new IOException(); + scene.EndAt = (double.Parse(parts[1]), double.Parse(parts[2])); + break; + default: throw new IOException(); + } + } + reader.Close(); + return scene ?? Default; + } + public void Write() + { + StreamWriter writer = new(FilePath ?? throw new IOException()); + + writer.WriteLine($"Scene {Width} by {Height}\n"); + foreach (ISceneObject obj in Objects) + { + if (obj is SceneRect objRect) + { + writer.WriteLine($"Rect {objRect.From.x} {objRect.From.y} to {objRect.To.x} {objRect.To.y}"); + } + else if (obj is SceneTri objTri) + { + writer.WriteLine($"Tri {objTri.A.x} {objTri.A.y} and {objTri.B.x} {objTri.B.y} and {objTri.C.x} {objTri.C.y}"); + } + else if (obj is SceneEllipse objEllipse) + { + writer.WriteLine($"Ellipse {objEllipse.Position.x} {objEllipse.Position.y} and {objEllipse.Size.x} by {objEllipse.Size.y}"); + } + } + + writer.Close(); + } + } +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneEllipse.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneEllipse.cs new file mode 100644 index 0000000..88dd4a0 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneEllipse.cs @@ -0,0 +1,24 @@ +using Nerd_STF.Mathematics; + +namespace AirTrajectoryBuilder.ObjectModels; + +public class SceneEllipse : ISceneObject +{ + public Float2 Position { get; set; } + public Float2 Size { get; set; } + + public bool Contains(Float2 point) + { + Float2 p = point - Position; + Float2 delta = p * 2 / Size; + + delta.x *= delta.x; + delta.y *= delta.y; + return delta.x + delta.y <= 1; + } + public ISceneObject DeepCopy() => new SceneEllipse() + { + Position = Position, + Size = Size + }; +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneRect.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneRect.cs new file mode 100644 index 0000000..622c484 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneRect.cs @@ -0,0 +1,24 @@ +using Nerd_STF.Mathematics; + +namespace AirTrajectoryBuilder.ObjectModels; + +public class SceneRect : ISceneObject +{ + public Float2 From { get; set; } + public Float2 To { get; set; } + + public bool Contains(Float2 p) + { + double minX = double.Min(From.x, To.x), maxX = double.Max(From.x, To.x), + minY = double.Min(From.y, To.y), maxY = double.Max(From.y, To.y); + + return p.x >= minX && p.x <= maxX && + p.y >= minY && p.y <= maxY; + } + + public ISceneObject DeepCopy() => new SceneRect() + { + From = From, + To = To + }; +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneTri.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneTri.cs new file mode 100644 index 0000000..b8d96cb --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SceneTri.cs @@ -0,0 +1,33 @@ +using Nerd_STF.Mathematics; +using System; + +namespace AirTrajectoryBuilder.ObjectModels; + +public class SceneTri : ISceneObject +{ + public Float2 A { get; set; } + public Float2 B { get; set; } + public Float2 C { get; set; } + + private static double Area(Float2 a, Float2 b, Float2 c) + { + return Math.Abs((a.x * (b.y - c.y) + + b.x * (c.y - a.y) + + c.x * (a.y - b.y)) * 0.5); + } + public bool Contains(Float2 p) + { + double area = Area(A, B, C), + a1 = Area(p, B, C), + a2 = Area(A, p, C), + a3 = Area(A, B, p); + return area == a1 + a2 + a3; + } + + public ISceneObject DeepCopy() => new SceneTri() + { + A = A, + B = B, + C = C + }; +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SimulationParameters.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SimulationParameters.cs new file mode 100644 index 0000000..94a4984 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SimulationParameters.cs @@ -0,0 +1,16 @@ +namespace AirTrajectoryBuilder.ObjectModels; + +public class SimulationParameters +{ + public required double StartAngle; + public required double StartVelocity; + public required double DeltaTime; + public required double Gravity; + public required double ToleranceSquared; + public required double ObjectRadius; + public required double DragCoefficient; + public required double Mass; + public required double AirDensity; + public required Scene Scene; + public required bool GenerateTable; +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SimulationResult.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SimulationResult.cs new file mode 100644 index 0000000..201915b --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SimulationResult.cs @@ -0,0 +1,24 @@ +using Nerd_STF.Mathematics; +using System.Collections.Generic; +using System.IO; + +namespace AirTrajectoryBuilder.ObjectModels; + +public class SimulationResult +{ + public double Duration; + + public double EndDistanceSquared; + + public double EndSpeed; + + public List Trail = []; + public List? Table = null; + + public SimulationParameters StartingConditions; + + public SimulationResult(SimulationParameters starting) + { + StartingConditions = starting; + } +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SweepParameters.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SweepParameters.cs new file mode 100644 index 0000000..ab284c4 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/SweepParameters.cs @@ -0,0 +1,18 @@ +namespace AirTrajectoryBuilder.ObjectModels; + +public class SweepParameters +{ + public required double AngleDelta, TimeDelta; + public required double SpeedMin, SpeedMax, SpeedDelta; + public required double Gravity; + public required double Tolerance; + + public required double ObjectRadius; + public required double DragCoefficient; + public required double Mass; + public required double AirDensity; + + public required ResultsFileMode FileMode; + + public required Scene Scene; +} diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/TableEntry.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/TableEntry.cs new file mode 100644 index 0000000..5a2b7af --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/ObjectModels/TableEntry.cs @@ -0,0 +1,5 @@ +using Nerd_STF.Mathematics; + +namespace AirTrajectoryBuilder.ObjectModels; + +public record class TableEntry(Float2 Position, Float2 Velocity, Float2 Acceleration); diff --git a/AirTrajectoryBuilder/AirTrajectoryBuilder/Program.cs b/AirTrajectoryBuilder/AirTrajectoryBuilder/Program.cs new file mode 100644 index 0000000..da3ee44 --- /dev/null +++ b/AirTrajectoryBuilder/AirTrajectoryBuilder/Program.cs @@ -0,0 +1,41 @@ +/**********722871********** + * Date: 12/2/2024 + * Programmer: Kyle Gilbert + * Program Name: AirTrajectoryBuilder + * Program Description: Sweeps possible launch angles and speeds in a given scene + * and applies aerodynamic physics. + **************************/ + +using AirTrajectoryBuilder.ObjectModels; +using System; +using System.Windows.Forms; + +namespace AirTrajectoryBuilder; + +public static class Program +{ + [STAThread] + public static void Main(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.SetHighDpiMode(HighDpiMode.SystemAware); + + Scene? scene; + if (args.Length > 0) + { + try + { + scene = Scene.Read(args[0]); + } + catch (Exception ex) + { + MessageBox.Show($"Error opening scene file: {ex.GetType().Name}"); + scene = null; + } + } + else scene = null; + + Application.Run(new MainForm(scene)); + } +} \ No newline at end of file diff --git a/README.md b/README.md index c64baf3..2adc427 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,12 @@ I have about 1-2 weeks for each project. Check the Git commits for specific date - Data transfer is automatically encrypted behind the scenes (though the server decrypts it when it gets it). - Allows for as many people to connect as need be. - The client is somewhat janky, but the server has zero issues from my testing. -- TypingTest +- TypingTest/ - A small game I made in like 2 hours. Using a list of words, it picks one at random and the user has to type it out. - It has a one-minute timer, and highlights the letters you got right. - Shows results as characters per minute, words per minute, and accuracy percentage at the end. +- AirTrajectoryBuilder + - A program I wrote that simulates the air trajectory of a projectile. Finished a while ago. + - Create a `.sce` file (a somewhat easy to use plain text format) + - Nice colors for each object. Scales seamlessly with a higher DPI. + - Sweeps possible angles and speeds to try and find the path that brings the ball closest to the end point.