From 0e509d62ef6fb273c2fff336ce46897519da4d13 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:40:33 +0000
Subject: [PATCH 1/4] Initial plan
From 9bb3dcef6ee123c9d61106b1475d595db6dbfa52 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:48:04 +0000
Subject: [PATCH 2/4] fix: address flyby timeline startup and selection issues
Agent-Logs-Url: https://github.com/TombEngine/Tomb-Editor/sessions/012116c1-b72a-4c43-a561-98abf2d02938
Co-authored-by: Nickelony <20436882+Nickelony@users.noreply.github.com>
---
.../FlybyTimeline/FlybyPreviewTests.cs | 25 +++++++++++++++++++
.../Controls/FlybyTimeline/FlybyConstants.cs | 5 +++-
.../FlybyTimeline/Preview/FlybyFrameState.cs | 2 +-
.../FlybyTimeline/Preview/FlybyPreview.cs | 15 +++++++----
.../UI/FlybyTimelineControl.Input.cs | 4 +--
.../UI/FlybyTimelineView.xaml.cs | 5 +++-
.../FlybyTimelineViewModel.Properties.cs | 2 +-
.../FlybyTimelineViewModel.Selection.cs | 18 ++-----------
TombEditor/Forms/FormFlybyCamera.Designer.cs | 6 ++---
TombEditor/Forms/FormFlybyCamera.cs | 3 ++-
TombEditor/ToolWindows/MainView.Designer.cs | 12 +++++++++
11 files changed, 66 insertions(+), 31 deletions(-)
diff --git a/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs b/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
index 5e87a42a50..4bab611be9 100644
--- a/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
+++ b/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using TombEditor.Controls.FlybyTimeline;
using TombEditor.Controls.FlybyTimeline.Preview;
using TombLib;
using TombLib.Graphics;
@@ -88,4 +89,28 @@ public void BeginExternalUpdate_AtEndOfSequenceMarksPreviewFinished()
Assert.IsTrue(preview.IsFinished);
Assert.AreEqual(preview.Cache.TotalDuration, preview.GetCurrentTimeSeconds(), 0.001f);
}
+
+ [TestMethod]
+ public void BuildViewProjection_ClampsFieldOfViewBelowOneEightyDegrees()
+ {
+ var savedCamera = new FreeCamera(Vector3.Zero, 0.0f, 0.0f, -MathF.PI * 0.5f, MathF.PI * 0.5f, MathC.DegToRad(60.0f));
+ using var preview = new FlybyPreview(savedCamera);
+ var previewCamera = new FreeCamera(Vector3.Zero, 0.0f, 0.0f, -MathF.PI * 0.5f, MathF.PI * 0.5f, MathC.DegToRad(60.0f));
+ var frame = new FlybyFrameState
+ {
+ Position = new Vector3(256.0f, 128.0f, 64.0f),
+ RotationY = 0.0f,
+ RotationX = 0.0f,
+ Fov = MathC.DegToRad(220.0f)
+ };
+
+ preview.SetStaticFrame(previewCamera, frame);
+
+ var viewProjection = preview.BuildViewProjection(1920.0f, 1080.0f, MathC.DegToRad(220.0f));
+
+ Assert.AreEqual(FlybyConstants.PreviewMaxFieldOfView, previewCamera.FieldOfView, 0.001f);
+ Assert.IsTrue(float.IsFinite(viewProjection.M11));
+ Assert.IsTrue(float.IsFinite(viewProjection.M22));
+ Assert.IsTrue(float.IsFinite(viewProjection.M33));
+ }
}
diff --git a/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs b/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
index d6f174a1e6..f09b790470 100644
--- a/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
+++ b/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
@@ -1,4 +1,5 @@
-using TombLib.LevelData;
+using TombLib;
+using TombLib.LevelData;
namespace TombEditor.Controls.FlybyTimeline;
@@ -61,6 +62,8 @@ public static class FlybyConstants
/// Lower bound for preview FOV values to avoid invalid or near-zero projection setup.
///
public const float PreviewMinFieldOfView = 0.01f;
+ public const float MaxFieldOfView = 179.99f;
+ public static readonly float PreviewMaxFieldOfView = MathC.DegToRad(MaxFieldOfView);
// Timeline constants.
diff --git a/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs b/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs
index 3873a14243..9719a339d0 100644
--- a/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs
+++ b/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs
@@ -26,6 +26,6 @@ public struct FlybyFrameState
RotationY = MathC.DegToRad(rotationY),
RotationX = -MathC.DegToRad(rotationX),
Roll = MathC.DegToRad(roll),
- Fov = MathC.DegToRad(fov)
+ Fov = MathC.DegToRad(Math.Min(fov, FlybyConstants.MaxFieldOfView))
};
}
diff --git a/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs b/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
index d29197149f..a9f05105aa 100644
--- a/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
+++ b/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
@@ -212,7 +212,7 @@ public static void ApplyFrame(Camera camera, FlybyFrameState frame)
camera.Position = frame.Position;
camera.RotationY = frame.RotationY;
camera.RotationX = frame.RotationX;
- camera.FieldOfView = frame.Fov;
+ camera.FieldOfView = ClampFieldOfView(frame.Fov);
var rotation = CreateFrameRotation(frame);
var look = MathC.HomogenousTransform(Vector3.UnitZ, rotation);
@@ -254,10 +254,7 @@ public Matrix4x4 BuildViewProjection(float width, float height, float defaultFov
}
var target = frame.Position + (Level.SectorSizeUnit * look);
- float fov = frame.Fov > FlybyConstants.PreviewMinFieldOfView ? frame.Fov : defaultFov;
-
- if (fov < FlybyConstants.PreviewMinFieldOfView)
- fov = MathC.DegToRad(80);
+ float fov = ClampFieldOfView(frame.Fov > FlybyConstants.PreviewMinFieldOfView ? frame.Fov : defaultFov);
var view = MathC.Matrix4x4CreateLookAtLH(frame.Position, target, up);
float aspectRatio = height != 0.0f ? width / height : 1.0f;
@@ -272,5 +269,13 @@ public Matrix4x4 BuildViewProjection(float width, float height, float defaultFov
private static Matrix4x4 CreateFrameRotation(FlybyFrameState frame)
=> Matrix4x4.CreateFromYawPitchRoll(frame.RotationY, frame.RotationX, 0);
+ private static float ClampFieldOfView(float fov)
+ {
+ if (!float.IsFinite(fov) || fov < FlybyConstants.PreviewMinFieldOfView)
+ return MathC.DegToRad(80.0f);
+
+ return Math.Min(fov, FlybyConstants.PreviewMaxFieldOfView);
+ }
+
#endregion Static frame helpers
}
diff --git a/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineControl.Input.cs b/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineControl.Input.cs
index f7991d4334..4b71146c32 100644
--- a/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineControl.Input.cs
+++ b/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineControl.Input.cs
@@ -277,8 +277,8 @@ private bool TryHandleTimelineShortcut(Key key)
///
/// Expands the viewport to show the full sequence range.
///
- public void ZoomToFit()
- => ApplyViewport(0.0f, _totalDurationSeconds * FlybyConstants.TimelineZoomOutScale, FlybyConstants.TimelineSmoothZoomEnabled);
+ public void ZoomToFit(bool smooth = FlybyConstants.TimelineSmoothZoomEnabled)
+ => ApplyViewport(0.0f, _totalDurationSeconds * FlybyConstants.TimelineZoomOutScale, smooth);
///
/// Pans the viewport left by one configured step.
diff --git a/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineView.xaml.cs b/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineView.xaml.cs
index ca93bf8988..ba80e1d9ec 100644
--- a/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineView.xaml.cs
+++ b/TombEditor/Controls/FlybyTimeline/UI/FlybyTimelineView.xaml.cs
@@ -17,6 +17,7 @@ public partial class FlybyTimelineView : UserControl
{
private FlybyTimelineViewModel? _viewModel;
private bool _zoomToFitQueued;
+ private bool _initialZoomToFitApplied;
///
/// Creates the timeline host control.
@@ -59,6 +60,7 @@ public void Cleanup()
_viewModel.Cleanup();
DataContext = null;
_viewModel = null;
+ _initialZoomToFitApplied = false;
}
///
@@ -168,7 +170,8 @@ private void QueueZoomToFit()
Dispatcher.BeginInvoke(DispatcherPriority.Background, new System.Action(() =>
{
_zoomToFitQueued = false;
- timelineControl.ZoomToFit();
+ timelineControl.ZoomToFit(_initialZoomToFitApplied);
+ _initialZoomToFitApplied = true;
}));
}
diff --git a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs
index 352c076385..47ed433f52 100644
--- a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs
+++ b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs
@@ -52,7 +52,7 @@ partial void OnCameraSpeedChanged(float value)
/// Applies a field-of-view edit to the selected camera.
///
partial void OnCameraFovChanged(float value)
- => ApplyPropertyToCamera(c => c.Fov = value, invalidateSequenceTiming: false, refreshTimeline: false);
+ => ApplyPropertyToCamera(c => c.Fov = Math.Min(value, FlybyConstants.MaxFieldOfView), invalidateSequenceTiming: false, refreshTimeline: false);
///
/// Applies a roll edit to the selected camera.
diff --git a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs
index 4c2a6d4c84..ea5032b6ff 100644
--- a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs
+++ b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs
@@ -179,7 +179,7 @@ private void SetSelectedCameras(IEnumerable cameras,
}
///
- /// Pushes current timeline selection into as an .
+ /// Pushes current timeline selection into .
///
private void SyncEditorSelection()
{
@@ -190,7 +190,7 @@ private void SyncEditorSelection()
try
{
- SetEditorSelection(GetMergedEditorSelection());
+ SetEditorSelection([.. _selectedCameras]);
}
finally
{
@@ -303,20 +303,6 @@ private static IReadOnlyList GetSelectedFlybyCameras(Object
return [];
}
- ///
- /// Merges timeline-selected cameras with non-flyby editor selection objects.
- ///
- private IReadOnlyList GetMergedEditorSelection()
- {
- var mergedSelection = GetEditorSelectionObjects()
- .Where(objectInstance => objectInstance is not FlybyCameraInstance)
- .ToList();
-
- mergedSelection.AddRange(_selectedCameras);
-
- return [.. mergedSelection.Distinct()];
- }
-
///
/// Returns the current editor selection as position-based objects.
///
diff --git a/TombEditor/Forms/FormFlybyCamera.Designer.cs b/TombEditor/Forms/FormFlybyCamera.Designer.cs
index 0b21d5229d..fc139acbec 100644
--- a/TombEditor/Forms/FormFlybyCamera.Designer.cs
+++ b/TombEditor/Forms/FormFlybyCamera.Designer.cs
@@ -391,10 +391,10 @@ private void InitializeComponent()
this.numFOV.Location = new System.Drawing.Point(82, 124);
this.numFOV.LoopValues = false;
this.numFOV.Maximum = new decimal(new int[] {
- 360,
+ 17999,
0,
0,
- 0});
+ 131072});
this.numFOV.Name = "numFOV";
this.numFOV.Size = new System.Drawing.Size(71, 22);
this.numFOV.TabIndex = 4;
@@ -561,4 +561,4 @@ private void InitializeComponent()
private DarkNumericUpDown numRotationX;
private DarkNumericUpDown numRotationY;
}
-}
\ No newline at end of file
+}
diff --git a/TombEditor/Forms/FormFlybyCamera.cs b/TombEditor/Forms/FormFlybyCamera.cs
index 8a9fdac063..4d92c2a922 100644
--- a/TombEditor/Forms/FormFlybyCamera.cs
+++ b/TombEditor/Forms/FormFlybyCamera.cs
@@ -11,6 +11,7 @@ namespace TombEditor.Forms
public partial class FormFlybyCamera : DarkForm
{
private const float ChangeComparisonEpsilon = 0.0001f;
+ private const decimal MaxFieldOfView = 179.99m;
private static readonly Size DefaultWindowSize = new Size(561, 461);
private static readonly Size CompactWindowSize = new Size(205, 319);
@@ -233,7 +234,7 @@ private void ApplyPendingValues(FlybyCameraInstance camera)
camera.Number = (ushort)numNumber.Value;
camera.Timer = (short)numTimer.Value;
camera.Speed = (float)numSpeed.Value;
- camera.Fov = (float)numFOV.Value;
+ camera.Fov = (float)Math.Min(numFOV.Value, MaxFieldOfView);
camera.Roll = (float)numRoll.Value;
camera.RotationX = (float)numRotationX.Value;
camera.RotationY = (float)numRotationY.Value;
diff --git a/TombEditor/ToolWindows/MainView.Designer.cs b/TombEditor/ToolWindows/MainView.Designer.cs
index 37347947bc..217d8a96ef 100644
--- a/TombEditor/ToolWindows/MainView.Designer.cs
+++ b/TombEditor/ToolWindows/MainView.Designer.cs
@@ -86,6 +86,7 @@ private void InitializeComponent()
panelStepHeightOptions = new System.Windows.Forms.Panel();
comboStepHeight = new DarkUI.Controls.DarkComboBox();
panelFlybyTimeline = new System.Windows.Forms.Panel();
+ panelFlybyTimelineDivider = new System.Windows.Forms.Panel();
flybyTimelineHost = new System.Windows.Forms.Integration.ElementHost();
flybyTimelineView = new Controls.FlybyTimeline.UI.FlybyTimelineView();
panelMainView = new System.Windows.Forms.Panel();
@@ -876,6 +877,7 @@ private void InitializeComponent()
// panelFlybyTimeline
//
panelFlybyTimeline.Controls.Add(flybyTimelineHost);
+ panelFlybyTimeline.Controls.Add(panelFlybyTimelineDivider);
panelFlybyTimeline.Dock = System.Windows.Forms.DockStyle.Top;
panelFlybyTimeline.Location = new System.Drawing.Point(5, 5);
panelFlybyTimeline.Name = "panelFlybyTimeline";
@@ -883,6 +885,15 @@ private void InitializeComponent()
panelFlybyTimeline.TabIndex = 17;
panelFlybyTimeline.Visible = false;
//
+ // panelFlybyTimelineDivider
+ //
+ panelFlybyTimelineDivider.BackColor = System.Drawing.Color.FromArgb(81, 81, 81);
+ panelFlybyTimelineDivider.Dock = System.Windows.Forms.DockStyle.Bottom;
+ panelFlybyTimelineDivider.Location = new System.Drawing.Point(0, 67);
+ panelFlybyTimelineDivider.Name = "panelFlybyTimelineDivider";
+ panelFlybyTimelineDivider.Size = new System.Drawing.Size(1495, 1);
+ panelFlybyTimelineDivider.TabIndex = 1;
+ //
// flybyTimelineHost
//
flybyTimelineHost.Dock = System.Windows.Forms.DockStyle.Fill;
@@ -1010,6 +1021,7 @@ private void InitializeComponent()
private DarkUI.Controls.DarkLabel lblStepHeight;
private DarkUI.Controls.DarkComboBox comboStepHeight;
private System.Windows.Forms.Panel panelFlybyTimeline;
+ private System.Windows.Forms.Panel panelFlybyTimelineDivider;
private System.Windows.Forms.Integration.ElementHost flybyTimelineHost;
private Controls.FlybyTimeline.UI.FlybyTimelineView flybyTimelineView;
private System.Windows.Forms.ToolStripButton butMirror;
From 909496bb60873d986e121842e64f0d227d7238f9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:52:37 +0000
Subject: [PATCH 3/4] chore: finalize timeline fixes and notes
Agent-Logs-Url: https://github.com/TombEngine/Tomb-Editor/sessions/012116c1-b72a-4c43-a561-98abf2d02938
Co-authored-by: Nickelony <20436882+Nickelony@users.noreply.github.com>
---
TombEditor/Controls/FlybyTimeline/FlybyConstants.cs | 13 +++++++++++++
.../Controls/FlybyTimeline/Preview/FlybyPreview.cs | 3 ++-
TombEditor/Forms/FormFlybyCamera.Designer.cs | 4 ++--
TombEditor/Forms/FormFlybyCamera.cs | 4 ++--
4 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs b/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
index f09b790470..787ac93f1a 100644
--- a/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
+++ b/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
@@ -62,7 +62,20 @@ public static class FlybyConstants
/// Lower bound for preview FOV values to avoid invalid or near-zero projection setup.
///
public const float PreviewMinFieldOfView = 0.01f;
+
+ ///
+ /// Upper bound for flyby camera field-of-view values, expressed in degrees.
+ ///
public const float MaxFieldOfView = 179.99f;
+
+ ///
+ /// Default preview field of view, expressed in degrees, used as a fallback when flyby data is invalid or non-finite.
+ ///
+ public const float DefaultFieldOfView = 80.0f;
+
+ ///
+ /// Upper bound for preview projection field-of-view values, expressed in radians.
+ ///
public static readonly float PreviewMaxFieldOfView = MathC.DegToRad(MaxFieldOfView);
// Timeline constants.
diff --git a/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs b/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
index a9f05105aa..a76cfd1683 100644
--- a/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
+++ b/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
@@ -269,10 +269,11 @@ public Matrix4x4 BuildViewProjection(float width, float height, float defaultFov
private static Matrix4x4 CreateFrameRotation(FlybyFrameState frame)
=> Matrix4x4.CreateFromYawPitchRoll(frame.RotationY, frame.RotationX, 0);
+ // Clamps preview FOV to a finite range accepted by perspective projection.
private static float ClampFieldOfView(float fov)
{
if (!float.IsFinite(fov) || fov < FlybyConstants.PreviewMinFieldOfView)
- return MathC.DegToRad(80.0f);
+ return MathC.DegToRad(FlybyConstants.DefaultFieldOfView);
return Math.Min(fov, FlybyConstants.PreviewMaxFieldOfView);
}
diff --git a/TombEditor/Forms/FormFlybyCamera.Designer.cs b/TombEditor/Forms/FormFlybyCamera.Designer.cs
index fc139acbec..bc26256ea2 100644
--- a/TombEditor/Forms/FormFlybyCamera.Designer.cs
+++ b/TombEditor/Forms/FormFlybyCamera.Designer.cs
@@ -391,10 +391,10 @@ private void InitializeComponent()
this.numFOV.Location = new System.Drawing.Point(82, 124);
this.numFOV.LoopValues = false;
this.numFOV.Maximum = new decimal(new int[] {
- 17999,
+ 360,
0,
0,
- 131072});
+ 0});
this.numFOV.Name = "numFOV";
this.numFOV.Size = new System.Drawing.Size(71, 22);
this.numFOV.TabIndex = 4;
diff --git a/TombEditor/Forms/FormFlybyCamera.cs b/TombEditor/Forms/FormFlybyCamera.cs
index 4d92c2a922..826215b069 100644
--- a/TombEditor/Forms/FormFlybyCamera.cs
+++ b/TombEditor/Forms/FormFlybyCamera.cs
@@ -11,7 +11,6 @@ namespace TombEditor.Forms
public partial class FormFlybyCamera : DarkForm
{
private const float ChangeComparisonEpsilon = 0.0001f;
- private const decimal MaxFieldOfView = 179.99m;
private static readonly Size DefaultWindowSize = new Size(561, 461);
private static readonly Size CompactWindowSize = new Size(205, 319);
@@ -42,6 +41,7 @@ public FormFlybyCamera(FlybyCameraInstance flyByCamera)
_editor = Editor.Instance;
InitializeComponent();
+ numFOV.Maximum = (decimal)FlybyConstants.MaxFieldOfView;
LoadWindowState();
}
@@ -234,7 +234,7 @@ private void ApplyPendingValues(FlybyCameraInstance camera)
camera.Number = (ushort)numNumber.Value;
camera.Timer = (short)numTimer.Value;
camera.Speed = (float)numSpeed.Value;
- camera.Fov = (float)Math.Min(numFOV.Value, MaxFieldOfView);
+ camera.Fov = (float)numFOV.Value;
camera.Roll = (float)numRoll.Value;
camera.RotationX = (float)numRotationX.Value;
camera.RotationY = (float)numRotationY.Value;
From daa37871f744a018b18091484231428ed740e427 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 20:11:27 +0000
Subject: [PATCH 4/4] fix: address flyby FOV review feedback
Agent-Logs-Url: https://github.com/TombEngine/Tomb-Editor/sessions/913d7b38-6fb6-4386-83bd-eb8c7a9c8695
Co-authored-by: Nickelony <20436882+Nickelony@users.noreply.github.com>
---
.../FlybyTimeline/FlybyPreviewTests.cs | 12 ++++++-
.../Controls/FlybyTimeline/FlybyConstants.cs | 31 ++++++++++++++++---
.../FlybyTimeline/Preview/FlybyFrameState.cs | 2 +-
.../FlybyTimeline/Preview/FlybyPreview.cs | 15 ++-------
.../FlybyTimelineViewModel.Properties.cs | 2 +-
.../FlybyTimelineViewModel.Selection.cs | 21 ++++---------
TombEditor/Forms/FormFlybyCamera.cs | 2 +-
7 files changed, 50 insertions(+), 35 deletions(-)
diff --git a/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs b/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
index 4bab611be9..d8db688c1e 100644
--- a/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
+++ b/TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
@@ -108,9 +108,19 @@ public void BuildViewProjection_ClampsFieldOfViewBelowOneEightyDegrees()
var viewProjection = preview.BuildViewProjection(1920.0f, 1080.0f, MathC.DegToRad(220.0f));
- Assert.AreEqual(FlybyConstants.PreviewMaxFieldOfView, previewCamera.FieldOfView, 0.001f);
+ Assert.AreEqual(FlybyConstants.MaxPreviewFieldOfViewRadians, previewCamera.FieldOfView, 0.001f);
Assert.IsTrue(float.IsFinite(viewProjection.M11));
Assert.IsTrue(float.IsFinite(viewProjection.M22));
Assert.IsTrue(float.IsFinite(viewProjection.M33));
}
+
+ [TestMethod]
+ public void FromDegrees_ClampsInvalidFieldOfViewIntoPreviewRange()
+ {
+ var lowFrame = FlybyFrameState.FromDegrees(Vector3.Zero, 0.0f, 0.0f, 0.0f, -15.0f);
+ var highFrame = FlybyFrameState.FromDegrees(Vector3.Zero, 0.0f, 0.0f, 0.0f, 220.0f);
+
+ Assert.AreEqual(MathC.DegToRad(FlybyConstants.DefaultPreviewFieldOfViewDegrees), lowFrame.Fov, 0.001f);
+ Assert.AreEqual(FlybyConstants.MaxPreviewFieldOfViewRadians, highFrame.Fov, 0.001f);
+ }
}
diff --git a/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs b/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
index 787ac93f1a..de72d30d82 100644
--- a/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
+++ b/TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
@@ -61,22 +61,45 @@ public static class FlybyConstants
///
/// Lower bound for preview FOV values to avoid invalid or near-zero projection setup.
///
- public const float PreviewMinFieldOfView = 0.01f;
+ public const float PreviewMinFieldOfViewRadians = 0.01f;
///
/// Upper bound for flyby camera field-of-view values, expressed in degrees.
///
- public const float MaxFieldOfView = 179.99f;
+ public const float MaxFlybyFieldOfViewDegrees = 179.99f;
///
/// Default preview field of view, expressed in degrees, used as a fallback when flyby data is invalid or non-finite.
///
- public const float DefaultFieldOfView = 80.0f;
+ public const float DefaultPreviewFieldOfViewDegrees = 80.0f;
///
/// Upper bound for preview projection field-of-view values, expressed in radians.
///
- public static readonly float PreviewMaxFieldOfView = MathC.DegToRad(MaxFieldOfView);
+ public static readonly float MaxPreviewFieldOfViewRadians = MathC.DegToRad(MaxFlybyFieldOfViewDegrees);
+
+ ///
+ /// Clamps a flyby camera field-of-view value expressed in degrees to the valid persisted range.
+ ///
+ public static float ClampFlybyFieldOfViewDegrees(float fovDegrees)
+ {
+ if (!float.IsFinite(fovDegrees))
+ return DefaultPreviewFieldOfViewDegrees;
+
+ return Math.Clamp(fovDegrees, 0.0f, MaxFlybyFieldOfViewDegrees);
+ }
+
+ ///
+ /// Clamps a preview field-of-view value expressed in radians to the valid projection range.
+ /// Invalid or too-small values fall back to the default preview field of view.
+ ///
+ public static float ClampPreviewFieldOfViewRadians(float fovRadians)
+ {
+ if (!float.IsFinite(fovRadians) || fovRadians < PreviewMinFieldOfViewRadians)
+ return MathC.DegToRad(DefaultPreviewFieldOfViewDegrees);
+
+ return Math.Min(fovRadians, MaxPreviewFieldOfViewRadians);
+ }
// Timeline constants.
diff --git a/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs b/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs
index 9719a339d0..1838555b21 100644
--- a/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs
+++ b/TombEditor/Controls/FlybyTimeline/Preview/FlybyFrameState.cs
@@ -26,6 +26,6 @@ public struct FlybyFrameState
RotationY = MathC.DegToRad(rotationY),
RotationX = -MathC.DegToRad(rotationX),
Roll = MathC.DegToRad(roll),
- Fov = MathC.DegToRad(Math.Min(fov, FlybyConstants.MaxFieldOfView))
+ Fov = FlybyConstants.ClampPreviewFieldOfViewRadians(MathC.DegToRad(fov))
};
}
diff --git a/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs b/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
index a76cfd1683..2535169deb 100644
--- a/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
+++ b/TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
@@ -212,7 +212,7 @@ public static void ApplyFrame(Camera camera, FlybyFrameState frame)
camera.Position = frame.Position;
camera.RotationY = frame.RotationY;
camera.RotationX = frame.RotationX;
- camera.FieldOfView = ClampFieldOfView(frame.Fov);
+ camera.FieldOfView = FlybyConstants.ClampPreviewFieldOfViewRadians(frame.Fov);
var rotation = CreateFrameRotation(frame);
var look = MathC.HomogenousTransform(Vector3.UnitZ, rotation);
@@ -254,7 +254,8 @@ public Matrix4x4 BuildViewProjection(float width, float height, float defaultFov
}
var target = frame.Position + (Level.SectorSizeUnit * look);
- float fov = ClampFieldOfView(frame.Fov > FlybyConstants.PreviewMinFieldOfView ? frame.Fov : defaultFov);
+ float fov = FlybyConstants.ClampPreviewFieldOfViewRadians(
+ frame.Fov > FlybyConstants.PreviewMinFieldOfViewRadians ? frame.Fov : defaultFov);
var view = MathC.Matrix4x4CreateLookAtLH(frame.Position, target, up);
float aspectRatio = height != 0.0f ? width / height : 1.0f;
@@ -268,15 +269,5 @@ public Matrix4x4 BuildViewProjection(float width, float height, float defaultFov
///
private static Matrix4x4 CreateFrameRotation(FlybyFrameState frame)
=> Matrix4x4.CreateFromYawPitchRoll(frame.RotationY, frame.RotationX, 0);
-
- // Clamps preview FOV to a finite range accepted by perspective projection.
- private static float ClampFieldOfView(float fov)
- {
- if (!float.IsFinite(fov) || fov < FlybyConstants.PreviewMinFieldOfView)
- return MathC.DegToRad(FlybyConstants.DefaultFieldOfView);
-
- return Math.Min(fov, FlybyConstants.PreviewMaxFieldOfView);
- }
-
#endregion Static frame helpers
}
diff --git a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs
index 47ed433f52..b3c9da8f18 100644
--- a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs
+++ b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Properties.cs
@@ -52,7 +52,7 @@ partial void OnCameraSpeedChanged(float value)
/// Applies a field-of-view edit to the selected camera.
///
partial void OnCameraFovChanged(float value)
- => ApplyPropertyToCamera(c => c.Fov = Math.Min(value, FlybyConstants.MaxFieldOfView), invalidateSequenceTiming: false, refreshTimeline: false);
+ => ApplyPropertyToCamera(c => c.Fov = FlybyConstants.ClampFlybyFieldOfViewDegrees(value), invalidateSequenceTiming: false, refreshTimeline: false);
///
/// Applies a roll edit to the selected camera.
diff --git a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs
index ea5032b6ff..ba8905be53 100644
--- a/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs
+++ b/TombEditor/Controls/FlybyTimeline/ViewModel/FlybyTimelineViewModel.Selection.cs
@@ -303,26 +303,17 @@ private static IReadOnlyList GetSelectedFlybyCameras(Object
return [];
}
- ///
- /// Returns the current editor selection as position-based objects.
- ///
- private IReadOnlyList GetEditorSelectionObjects()
- {
- if (_editor.SelectedObject is ObjectGroup group)
- return [.. group.Cast()];
-
- if (_editor.SelectedObject is PositionBasedObjectInstance positionBased)
- return [positionBased];
-
- return [];
- }
-
///
/// Applies a new selection back into the editor.
///
private void SetEditorSelection(IReadOnlyList selectedObjects)
{
- var currentSelection = GetEditorSelectionObjects();
+ IReadOnlyList currentSelection = _editor.SelectedObject switch
+ {
+ ObjectGroup group => [.. group.Cast()],
+ PositionBasedObjectInstance positionBased => [positionBased],
+ _ => []
+ };
if (currentSelection.Count == selectedObjects.Count && currentSelection.All(selectedObjects.Contains))
return;
diff --git a/TombEditor/Forms/FormFlybyCamera.cs b/TombEditor/Forms/FormFlybyCamera.cs
index 826215b069..3e0f188f1f 100644
--- a/TombEditor/Forms/FormFlybyCamera.cs
+++ b/TombEditor/Forms/FormFlybyCamera.cs
@@ -41,7 +41,7 @@ public FormFlybyCamera(FlybyCameraInstance flyByCamera)
_editor = Editor.Instance;
InitializeComponent();
- numFOV.Maximum = (decimal)FlybyConstants.MaxFieldOfView;
+ numFOV.Maximum = (decimal)FlybyConstants.MaxFlybyFieldOfViewDegrees;
LoadWindowState();
}