Files
tooll3--t3/Editor/Gui/Windows/Output/CameraSelectionHandling.cs
2026-07-13 13:13:17 +08:00

446 lines
16 KiB
C#

#nullable enable
using ImGuiNET;
using T3.Core.Animation;
using T3.Core.DataTypes;
using T3.Core.Operator;
using T3.Core.Operator.Interfaces;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Interaction.Camera;
using T3.Editor.Gui.Interaction.TransformGizmos;
using T3.Editor.Gui.Styling;
using T3.Editor.UiModel.ProjectHandling;
using T3.Editor.UiModel.Selection;
namespace T3.Editor.Gui.Windows.Output;
/// <summary>
/// Handles switching and pinning of interactive <see cref="ICamera"/>s for views.
/// It uses four different modes and draws the dropdown to switch between them
/// </summary>
internal sealed class CameraSelectionHandling
{
public ICamera? CameraForRendering { get; private set; }
public bool BypassCamera { get; private set; }
public bool PreventCameraInteraction { get; private set; }
public bool PreventImageCanvasInteraction { get; private set; }
private enum ControlModes
{
/// <summary>
/// If selected op is Camera, manipulate this.
/// else
/// if playback is running
/// move ViewerCamera to first camera in graph
/// else
/// manipulate ViewerCamera
/// </summary>
SceneViewerFollowing,
/// <summary>
/// Always use 3rd person view of ViewerCamera, regardless of selected Operator.
/// </summary>
UseViewer,
/// <summary>
/// If rendered, op is Command-Type and manipulate first found camera in graph
/// or if rendered Op is ImageType and selected Op is Camera manipulate camera
/// </summary>
AutoUseFirstCam,
/// <summary>
/// Uses the camera picked from dropdown for manipulation.
/// If camera wasn't recently updated, fall back to last other mode.
/// </summary>
PickedACamera,
}
public CameraSelectionHandling()
{
_getPlayback = () => Playback.Current;
}
public CameraSelectionHandling(NodeSelection nodeSelection, Func<Playback> getPlayback)
{
_nodeSelection = nodeSelection;
_getPlayback = getPlayback;
}
private readonly NodeSelection? _nodeSelection;
// preparation for each window to have its own playback
private readonly Func<Playback> _getPlayback;
private NodeSelection? NodeSelection => _nodeSelection ?? ProjectView.Focused?.NodeSelection;
public void Update(Instance? drawnInstance, Type? drawnType, bool preventInteractions = false)
{
var currentPlayback = _getPlayback();
var timeInBars = currentPlayback.TimeInBars;
_hasAnimationTimeChanged = Math.Abs(_lastUpdateTime - timeInBars) > 0.001f;
_lastUpdateTime = timeInBars;
PreventImageCanvasInteraction = false;
_drawnInstance = drawnInstance;
if (drawnInstance == null)
{
PreventCameraInteraction = true;
CameraForRendering = _outputWindowViewCamera;
_recentlyUsedCameras.Clear();
return;
}
// Update recently used cameras (this is expensive!)
UpdateRecentCameras(drawnInstance);
ICamera? cameraForManipulation = null;
CameraForRendering = null;
PreventCameraInteraction = preventInteractions;
if (_controlMode == ControlModes.PickedACamera)
{
var isPickStillValid = false;
foreach (var recentCam in _recentlyUsedCameras)
{
if (recentCam is not Instance camInstance)
continue;
if (camInstance.SymbolChildId == _pickedCameraId)
{
cameraForManipulation = recentCam;
CameraForRendering = _outputWindowViewCamera;
isPickStillValid = true;
break;
}
}
if (!isPickStillValid)
{
_controlMode = _lastControlMode;
}
}
_drawnTypeIsCommand = drawnType == typeof(Command);
switch (_controlMode)
{
case ControlModes.SceneViewerFollowing:
{
if (!_drawnTypeIsCommand)
{
PreventCameraInteraction = true;
}
cameraForManipulation = _outputWindowViewCamera;
if (_firstCamInGraph != null)
{
PreventImageCanvasInteraction = true;
var isCamOpSelected = IsCamOpSelected();
if (isCamOpSelected)
{
cameraForManipulation = _firstCamInGraph;
}
if (_hasAnimationTimeChanged)
{
_outputWindowViewCamera.CameraPosition = _firstCamInGraph.CameraPosition;
_outputWindowViewCamera.CameraTarget = _firstCamInGraph.CameraTarget;
_outputWindowViewCamera.CameraRoll = _firstCamInGraph.CameraRoll;
}
}
BypassCamera = true;
break;
}
case ControlModes.UseViewer:
cameraForManipulation = _outputWindowViewCamera;
BypassCamera = true;
break;
case ControlModes.AutoUseFirstCam:
{
if (_firstCamInGraph != null)
{
var isCamOpSelected = IsCamOpSelected();
if (!isCamOpSelected)
{
if (_drawnTypeIsCommand)
{
cameraForManipulation = _outputWindowViewCamera;
}
else
{
cameraForManipulation = null;
}
}
else
{
PreventImageCanvasInteraction = true;
cameraForManipulation = _firstCamInGraph;
}
CameraForRendering = _outputWindowViewCamera;
//CameraInteraction.ResetCamera(_outputWindowViewCamera);
}
else
{
cameraForManipulation = _outputWindowViewCamera;
}
BypassCamera = false;
break;
}
case ControlModes.PickedACamera:
PreventImageCanvasInteraction = true;
break;
}
if (_controlMode != ControlModes.PickedACamera)
{
//CameraForRendering = cameraForManipulation;
}
else
{
//PreventImageCanvasInteraction = true;
}
CameraForRendering ??= _outputWindowViewCamera;
if (TransformGizmoHandling.IsDragging)
PreventCameraInteraction = true;
if (cameraForManipulation != null)
{
_cameraInteraction.Update(cameraForManipulation, !PreventCameraInteraction);
}
bool IsCamOpSelected()
{
var firstSelectedInstance = NodeSelection?.GetSelectedInstanceWithoutComposition();
return firstSelectedInstance is ICamera camera && camera == _firstCamInGraph;
}
}
/// <summary>
/// Searches for all recently updated (evaluated) camera ops in the current operator
/// </summary>
private void UpdateRecentCameras(Instance drawnInstance)
{
_recentlyUsedCameras.Clear();
var parentInstance = drawnInstance.Parent;
if (parentInstance != null)
{
var children = parentInstance.Children.Values;
foreach (var child in children)
{
if (child is not ICamera cam2)
continue;
if (child.Outputs[0].DirtyFlag.FramesSinceLastUpdate > 1)
continue;
_recentlyUsedCameras.Add(cam2);
}
}
var selectedInstance = NodeSelection?.GetSelectedInstanceWithoutComposition();
if (selectedInstance is ICamera selectedCamera
&& !_recentlyUsedCameras.Contains(selectedCamera))
{
_recentlyUsedCameras.Add(selectedCamera);
}
_firstCamInGraph = _recentlyUsedCameras.Count > 0 ? _recentlyUsedCameras[0] : null;
}
public void DrawCameraControlSelection()
{
if (_drawnInstance == null)
return;
ImGui.SetNextItemWidth(145*T3Ui.UiScaleFactor);
var label = String.Empty;
switch (_controlMode)
{
case ControlModes.AutoUseFirstCam:
label = "Auto";
break;
case ControlModes.SceneViewerFollowing:
label = "Follow";
break;
case ControlModes.UseViewer:
label = "Viewer";
break;
case ControlModes.PickedACamera:
label = "Locked to Op";
break;
}
var isAuto = _controlMode == ControlModes.AutoUseFirstCam;
var width = ImGui.GetFrameHeight() * 5;
ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
var labelColor = isAuto ? UiColors.TextMuted : UiColors.ForegroundFull;
ImGui.PushStyleColor(ImGuiCol.Text, labelColor.Rgba);
var isClicked = ImGui.Button($" {label}##CameraMode", new Vector2(width, ImGui.GetFrameHeight()));
ImGui.PopStyleColor();
ImGui.PopStyleVar();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
if (isClicked)
{
ImGui.OpenPopup("cameraPopup");
ImGui.SetNextWindowPos(new Vector2(min.X, max.Y));
}
var verticalOffset = (ImGui.GetFrameHeight() - Icons.FontSize) /2 + 1;
Icons.DrawIconAtScreenPosition(Icon.Camera, min + new Vector2(5 * T3Ui.UiScaleFactor, verticalOffset), ImGui.GetWindowDrawList(), labelColor);
if (ImGui.BeginPopup("cameraPopup", ImGuiWindowFlags.None))
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(6, 6) * T3Ui.UiScaleFactor);
if(CustomComponents.DrawMenuItem(42+0, "Auto",
isChecked:_controlMode == ControlModes.AutoUseFirstCam,
isEnabled:true))
{
_controlMode = ControlModes.AutoUseFirstCam;
_pickedCameraId = Guid.Empty;
}
CustomComponents.TooltipForLastItem("Shows the first scene camera. For manipulation select the camera operator in graph or in the list below.");
if (ImGui.IsItemHovered() && _firstCamInGraph is Instance camInstance && NodeSelection != null)
FrameStats.AddHoveredId(camInstance.SymbolChildId);
if(CustomComponents.DrawMenuItem(42+1, "Viewer",
isChecked:_controlMode == ControlModes.UseViewer,
isEnabled:_drawnTypeIsCommand))
{
_controlMode = ControlModes.UseViewer;
_pickedCameraId = Guid.Empty;
}
CustomComponents.TooltipForLastItem("Ignores scene cameras. This can be useful in combination with [ShowCamGizmos].");
if(CustomComponents.DrawMenuItem(42+2, "Viewer (Following)",
isChecked:_controlMode == ControlModes.SceneViewerFollowing,
isEnabled:_drawnTypeIsCommand))
{
_controlMode = ControlModes.SceneViewerFollowing;
_pickedCameraId = Guid.Empty;
}
if (ImGui.IsItemHovered() && _firstCamInGraph is Instance camInstance2 && NodeSelection != null)
FrameStats.AddHoveredId(camInstance2.SymbolChildId);
CustomComponents
.TooltipForLastItem("During playback the scene viewer is following the scene camera. Otherwise it can be independently manipulated without affecting the scene camera.");
if (_recentlyUsedCameras.Count > 0)
{
ImGui.Separator();
CustomComponents.MenuGroupHeader("Active Cameras");
}
if (_drawnInstance?.Parent != null)
{
for (var index = 0; index < _recentlyUsedCameras.Count; index++)
{
var cam = _recentlyUsedCameras[index];
if (cam is not Instance cameraInstance ||
!_drawnInstance.Parent.Symbol.Children.TryGetValue(cameraInstance.SymbolChildId, out var symbolChild))
continue;
ImGui.PushID(cameraInstance.SymbolChildId.GetHashCode());
{
if (symbolChild != null
&& CustomComponents.DrawMenuItem(9999+index, symbolChild.ReadableName, isChecked:cameraInstance.SymbolChildId == _pickedCameraId)
)
{
_lastControlMode = _controlMode;
_controlMode = ControlModes.PickedACamera;
_pickedCameraId = cameraInstance.SymbolChildId;
GraphCanvasUtils.SelectAndCenterChildIdInView(symbolChild.Id);
}
if (ImGui.IsItemHovered() && NodeSelection != null)
{
FrameStats.AddHoveredId(cameraInstance.SymbolChildId);
}
}
ImGui.PopID();
}
}
ImGui.PopStyleVar();
ImGui.EndPopup();
}
else
{
CustomComponents.TooltipForLastItem("Camera control mode",
"This affects which camera will be manipulated by the view controls. Please also review to the tooltips of the dropdown options.");
}
ImGui.SameLine();
}
public void ResetView()
{
_cameraInteraction.ResetView();
}
internal void SaveStateTo(OutputWindowState state)
{
state.CameraControlMode = (OutputWindowState.CameraControlModes)(int)_controlMode;
state.CameraPosition = [_outputWindowViewCamera.CameraPosition.X, _outputWindowViewCamera.CameraPosition.Y, _outputWindowViewCamera.CameraPosition.Z];
state.CameraTarget = [_outputWindowViewCamera.CameraTarget.X, _outputWindowViewCamera.CameraTarget.Y, _outputWindowViewCamera.CameraTarget.Z];
state.CameraRoll = _outputWindowViewCamera.CameraRoll;
}
internal void LoadStateFrom(OutputWindowState state)
{
_controlMode = (ControlModes)(int)state.CameraControlMode;
if (state.CameraPosition.Length == 3)
{
_outputWindowViewCamera.CameraPosition = new Vector3(state.CameraPosition[0], state.CameraPosition[1], state.CameraPosition[2]);
}
if (state.CameraTarget.Length == 3)
{
_outputWindowViewCamera.CameraTarget = new Vector3(state.CameraTarget[0], state.CameraTarget[1], state.CameraTarget[2]);
}
_outputWindowViewCamera.CameraRoll = state.CameraRoll;
_cameraInteraction.SetView(_outputWindowViewCamera);
}
private bool _drawnTypeIsCommand;
private ControlModes _controlMode = ControlModes.AutoUseFirstCam;
private readonly ViewCamera _outputWindowViewCamera = new();
private readonly CameraInteraction _cameraInteraction = new();
private double _lastUpdateTime;
private bool _hasAnimationTimeChanged;
private Guid _pickedCameraId = Guid.Empty;
private Instance? _drawnInstance;
private readonly List<ICamera> _recentlyUsedCameras = [];
private ControlModes _lastControlMode;
private ICamera? _firstCamInGraph;
}