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.