Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions TombEditor.Tests/FlybyTimeline/FlybyPreviewTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Numerics;
using TombEditor.Controls.FlybyTimeline;
using TombEditor.Controls.FlybyTimeline.Preview;
using TombLib;
using TombLib.Graphics;
Expand Down Expand Up @@ -88,4 +89,38 @@ 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.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);
}
}
43 changes: 41 additions & 2 deletions TombEditor/Controls/FlybyTimeline/FlybyConstants.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using TombLib.LevelData;
using TombLib;
using TombLib.LevelData;

namespace TombEditor.Controls.FlybyTimeline;

Expand Down Expand Up @@ -60,7 +61,45 @@ public static class FlybyConstants
/// <summary>
/// Lower bound for preview FOV values to avoid invalid or near-zero projection setup.
/// </summary>
public const float PreviewMinFieldOfView = 0.01f;
public const float PreviewMinFieldOfViewRadians = 0.01f;

/// <summary>
/// Upper bound for flyby camera field-of-view values, expressed in degrees.
/// </summary>
public const float MaxFlybyFieldOfViewDegrees = 179.99f;

/// <summary>
/// Default preview field of view, expressed in degrees, used as a fallback when flyby data is invalid or non-finite.
/// </summary>
public const float DefaultPreviewFieldOfViewDegrees = 80.0f;

/// <summary>
/// Upper bound for preview projection field-of-view values, expressed in radians.
/// </summary>
public static readonly float MaxPreviewFieldOfViewRadians = MathC.DegToRad(MaxFlybyFieldOfViewDegrees);

/// <summary>
/// Clamps a flyby camera field-of-view value expressed in degrees to the valid persisted range.
/// </summary>
public static float ClampFlybyFieldOfViewDegrees(float fovDegrees)
{
if (!float.IsFinite(fovDegrees))
return DefaultPreviewFieldOfViewDegrees;

return Math.Clamp(fovDegrees, 0.0f, MaxFlybyFieldOfViewDegrees);
}

/// <summary>
/// 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.
/// </summary>
public static float ClampPreviewFieldOfViewRadians(float fovRadians)
{
if (!float.IsFinite(fovRadians) || fovRadians < PreviewMinFieldOfViewRadians)
return MathC.DegToRad(DefaultPreviewFieldOfViewDegrees);

return Math.Min(fovRadians, MaxPreviewFieldOfViewRadians);
}

// Timeline constants.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public struct FlybyFrameState
RotationY = MathC.DegToRad(rotationY),
RotationX = -MathC.DegToRad(rotationX),
Roll = MathC.DegToRad(roll),
Fov = MathC.DegToRad(fov)
Comment thread
Nickelony marked this conversation as resolved.
Fov = FlybyConstants.ClampPreviewFieldOfViewRadians(MathC.DegToRad(fov))
};
}
9 changes: 3 additions & 6 deletions TombEditor/Controls/FlybyTimeline/Preview/FlybyPreview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = FlybyConstants.ClampPreviewFieldOfViewRadians(frame.Fov);

var rotation = CreateFrameRotation(frame);
var look = MathC.HomogenousTransform(Vector3.UnitZ, rotation);
Expand Down Expand Up @@ -254,10 +254,8 @@ 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 = 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;
Expand All @@ -271,6 +269,5 @@ public Matrix4x4 BuildViewProjection(float width, float height, float defaultFov
/// </summary>
private static Matrix4x4 CreateFrameRotation(FlybyFrameState frame)
=> Matrix4x4.CreateFromYawPitchRoll(frame.RotationY, frame.RotationX, 0);

#endregion Static frame helpers
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ private bool TryHandleTimelineShortcut(Key key)
/// <summary>
/// Expands the viewport to show the full sequence range.
/// </summary>
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);

/// <summary>
/// Pans the viewport left by one configured step.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public partial class FlybyTimelineView : UserControl
{
private FlybyTimelineViewModel? _viewModel;
private bool _zoomToFitQueued;
private bool _initialZoomToFitApplied;

/// <summary>
/// Creates the timeline host control.
Expand Down Expand Up @@ -59,6 +60,7 @@ public void Cleanup()
_viewModel.Cleanup();
DataContext = null;
_viewModel = null;
_initialZoomToFitApplied = false;
}

/// <summary>
Expand Down Expand Up @@ -168,7 +170,8 @@ private void QueueZoomToFit()
Dispatcher.BeginInvoke(DispatcherPriority.Background, new System.Action(() =>
{
_zoomToFitQueued = false;
timelineControl.ZoomToFit();
timelineControl.ZoomToFit(_initialZoomToFitApplied);
_initialZoomToFitApplied = true;
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ partial void OnCameraSpeedChanged(float value)
/// Applies a field-of-view edit to the selected camera.
/// </summary>
partial void OnCameraFovChanged(float value)
=> ApplyPropertyToCamera(c => c.Fov = value, invalidateSequenceTiming: false, refreshTimeline: false);
=> ApplyPropertyToCamera(c => c.Fov = FlybyConstants.ClampFlybyFieldOfViewDegrees(value), invalidateSequenceTiming: false, refreshTimeline: false);

/// <summary>
/// Applies a roll edit to the selected camera.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private void SetSelectedCameras(IEnumerable<FlybyCameraInstance> cameras,
}

/// <summary>
/// Pushes current timeline selection into <see cref="Editor.SelectedObject"/> as an <see cref="ObjectGroup"/>.
/// Pushes current timeline selection into <see cref="Editor.SelectedObject"/>.
/// </summary>
private void SyncEditorSelection()
{
Expand All @@ -190,7 +190,7 @@ private void SyncEditorSelection()

try
{
SetEditorSelection(GetMergedEditorSelection());
SetEditorSelection([.. _selectedCameras]);
}
finally
{
Expand Down Expand Up @@ -303,40 +303,17 @@ private static IReadOnlyList<FlybyCameraInstance> GetSelectedFlybyCameras(Object
return [];
}

/// <summary>
/// Merges timeline-selected cameras with non-flyby editor selection objects.
/// </summary>
private IReadOnlyList<PositionBasedObjectInstance> GetMergedEditorSelection()
{
var mergedSelection = GetEditorSelectionObjects()
Comment thread
Nickelony marked this conversation as resolved.
.Where(objectInstance => objectInstance is not FlybyCameraInstance)
.ToList();

mergedSelection.AddRange(_selectedCameras);

return [.. mergedSelection.Distinct()];
}

/// <summary>
/// Returns the current editor selection as position-based objects.
/// </summary>
private IReadOnlyList<PositionBasedObjectInstance> GetEditorSelectionObjects()
{
if (_editor.SelectedObject is ObjectGroup group)
return [.. group.Cast<PositionBasedObjectInstance>()];

if (_editor.SelectedObject is PositionBasedObjectInstance positionBased)
return [positionBased];

return [];
}

/// <summary>
/// Applies a new selection back into the editor.
/// </summary>
private void SetEditorSelection(IReadOnlyList<PositionBasedObjectInstance> selectedObjects)
{
var currentSelection = GetEditorSelectionObjects();
IReadOnlyList<PositionBasedObjectInstance> currentSelection = _editor.SelectedObject switch
{
ObjectGroup group => [.. group.Cast<PositionBasedObjectInstance>()],
PositionBasedObjectInstance positionBased => [positionBased],
_ => []
};

if (currentSelection.Count == selectedObjects.Count && currentSelection.All(selectedObjects.Contains))
return;
Expand Down
2 changes: 1 addition & 1 deletion TombEditor/Forms/FormFlybyCamera.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions TombEditor/Forms/FormFlybyCamera.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public FormFlybyCamera(FlybyCameraInstance flyByCamera)
_editor = Editor.Instance;

InitializeComponent();
numFOV.Maximum = (decimal)FlybyConstants.MaxFlybyFieldOfViewDegrees;

LoadWindowState();
}
Expand Down
12 changes: 12 additions & 0 deletions TombEditor/ToolWindows/MainView.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.