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(); }