Files
2026-07-13 13:13:17 +08:00

721 lines
29 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#nullable enable
using System.Diagnostics.CodeAnalysis;
using ImGuiNET;
using T3.Core.DataTypes;
using T3.Core.DataTypes.Vector;
using T3.Core.Operator;
using T3.Core.Video;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Interaction.Keyboard;
using T3.Editor.Gui.OutputUi;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.Gui.Windows.Layouts;
using T3.Editor.Gui.Windows.RenderExport;
using T3.Editor.UiModel;
using T3.Editor.UiModel.ProjectHandling;
using SkillTraining = T3.Editor.Skills.Training.SkillTraining;
using Texture2D = T3.Core.DataTypes.Texture2D;
using Vector2 = System.Numerics.Vector2;
namespace T3.Editor.Gui.Windows.Output;
[HelpUiID("OutputWindow")]
internal sealed class OutputWindow : Window
{
#region Window implementation
public OutputWindow()
{
Config.Title = LayoutHandling.OutputPrefix + _instanceCounter;
Config.Visible = true;
AllowMultipleInstances = true;
Config.Visible = true;
WindowFlags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
_instanceCounter++;
_camSelectionHandling = new CameraSelectionHandling();
OutputWindowInstances.Add(this);
}
public static bool TryGetPrimaryOutputWindow([NotNullWhen(true)] out OutputWindow? outputWindow)
{
foreach (var window in OutputWindowInstances)
{
if (!window.Config.Visible)
continue;
if (window is not OutputWindow outputWindow2)
continue;
outputWindow = outputWindow2;
return true;
}
outputWindow = null;
return false;
}
public Texture2D? GetCurrentTexture()
{
return _imageCanvas.LastTexture;
}
protected override void Close()
{
SaveStateToProject();
OutputWindowInstances.Remove(this);
}
protected override void AddAnotherInstance()
{
// ReSharper disable once ObjectCreationAsStatement
new OutputWindow();
}
internal override List<Window> GetInstances()
{
return OutputWindowInstances;
}
#endregion
protected override void DrawContent()
{
SyncStateWithProject();
// In skill training the toolbar is hidden, so force the window-fitting resolution
// instead of whatever the level project saved (typically 1920x1080).
if (SkillTraining.IsInPlayMode && _selectedResolution != ResolutionHandling.DefaultResolution)
_selectedResolution = ResolutionHandling.DefaultResolution;
// Sync copy-based fields to State every frame so saves always capture current values
SyncCopyFieldsToState();
ImGui.BeginChild("##content",
new Vector2(0, ImGui.GetWindowHeight()),
ImGuiChildFlags.None,
ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoScrollWithMouse |
ImGuiWindowFlags.NoBackground
);
{
// Very ugly hack to prevent scaling the output above window size
var keepScale = T3Ui.UiScaleFactor;
// Draw output
_imageCanvas.SetAsCurrent();
// Move down to avoid overlapping with the toolbar
ImGui.SetCursorPos(ImGui.GetCursorStartPos() + new Vector2(0, 40));
// ImGui 1.91 sets an internal IsSetPos flag on SetCursorPos and asserts in End()
// if no item is submitted afterwards. The image canvas draws to the raw draw list
// and does not emit items, so submit an empty Dummy as an extent marker.
ImGui.Dummy(Vector2.Zero);
Pinning.TryGetPinnedOrSelectedInstance(out var drawnInstance, out var graphCanvas);
if (graphCanvas != null)
{
Pinning.TryGetPinnedEvaluationInstance(graphCanvas.Structure, out var evaluationInstance);
var drawnType = UpdateAndDrawOutput(drawnInstance, evaluationInstance);
ImageOutputCanvas.Deactivate();
_camSelectionHandling.Update(drawnInstance, drawnType);
var editingFlags = _camSelectionHandling.PreventCameraInteraction
| _camSelectionHandling.PreventImageCanvasInteraction
| SkillTraining.IsInPlayMode
| drawnType != typeof(Texture2D)
? T3Ui.EditingFlags.PreventMouseInteractions
: T3Ui.EditingFlags.None;
if ((editingFlags & T3Ui.EditingFlags.PreventMouseInteractions) != 0)
T3Ui.UiScaleFactor = 1;
_imageCanvas.Update(editingFlags);
T3Ui.UiScaleFactor = keepScale;
if (UserActions.FocusSelection.Triggered())
{
if (drawnType == typeof(Texture2D))
{
_imageCanvas.SetViewMode(ImageOutputCanvas.Modes.Fitted);
}
else if (drawnType == typeof(Command))
{
_camSelectionHandling.ResetView();
}
}
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Text.Rgba);
ImGui.PushStyleColor(ImGuiCol.FrameBg, UiColors.BackgroundButton.Rgba);
ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, UiColors.BackgroundHover.Rgba);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UiColors.BackgroundHover.Rgba);
ImGui.PushStyleColor(ImGuiCol.ChildBg, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ScrollbarBg, new Vector4(0.3f, 0.3f, 0.3f, 0.1f));
if (!SkillTraining.IsInPlayMode)
{
DrawToolbar(drawnType);
DrawRenderProgressBar();
}
ImGui.PopStyleColor(6);
}
CustomComponents.DrawWindowFocusFrame();
}
ImGui.EndChild();
}
private void DrawToolbar(Type? drawnType)
{
// Set cursor to top of the window
ImGui.SetCursorPos(ImGui.GetCursorStartPos());
// Calculate available width
var availableWidth = ImGui.GetWindowSize().X;
var toolbarHeight = ImGui.GetTextLineHeight() + 22;
// Begin a horizontally scrollable child region
ImGui.BeginChild("##toolbar_scroll", new Vector2(availableWidth, toolbarHeight), ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar);
// Keep filled backgrounds so the toolbar reads as a continuous bar.
CustomComponents.PushToolbarIconBackground();
Pinning.DrawPinning();
if (CustomComponents.StateButton("1:1",
Math.Abs(_imageCanvas.Scale.X - 1f) < 0.001f
? CustomComponents.ButtonStates.Disabled
: CustomComponents.ButtonStates.Emphasized))
{
_imageCanvas.SetScaleToMatchPixels();
_imageCanvas.SetViewMode(ImageOutputCanvas.Modes.Pixel);
}
ImGui.SameLine();
{
if (CustomComponents.StateButton("Fit",
_imageCanvas.ViewMode == ImageOutputCanvas.Modes.Fitted
? CustomComponents.ButtonStates.Disabled
: CustomComponents.ButtonStates.Emphasized))
{
if (drawnType == typeof(Texture2D))
{
_imageCanvas.SetViewMode(ImageOutputCanvas.Modes.Fitted);
}
else if (drawnType == typeof(Command))
{
_camSelectionHandling.ResetView();
}
}
var label = drawnType == typeof(Texture2D) ? "Fit image to view" : "Reset view or camera position";
var shortCut = UserActions.FocusSelection.ListShortcuts();
CustomComponents.TooltipForLastItem(label, shortCut);
}
// Show gizmos
{
ImGui.SameLine();
var showGizmos = State.ShowGizmos != GizmoVisibility.Off;
if (CustomComponents.ToggleIconButton(ref showGizmos, Icon.Grid, Vector2.One * ImGui.GetFrameHeight()))
{
State.ShowGizmos = showGizmos
? GizmoVisibility.On
: GizmoVisibility.Off;
FlagSymbolUiAsModified();
}
CustomComponents.TooltipForLastItem("Toggle gizmos and floor grid.",
"Gizmos are available for selected transform operators and can be dragged to adjust their position.");
}
// Gizmo Transform mode
if (State.ShowGizmos != GizmoVisibility.Off)
{
var size = Vector2.One * ImGui.GetFrameHeight(); // Calculate before pushing font
var icon = State.TransformGizmoMode switch
{
TransformGizmoModes.None => "" + (char)Icon.Hidden,
TransformGizmoModes.Select => "" + (char)Icon.Pipette,
TransformGizmoModes.Move => "" + (char)Icon.Move,
TransformGizmoModes.Rotate => "" + (char)Icon.Rotate,
TransformGizmoModes.Scale => "" + (char)Icon.Scale,
_ => throw new ArgumentOutOfRangeException()
};
ImGui.SameLine();
ImGui.PushFont(Icons.IconFont);
if (ImGui.Button(icon, size))
ImGui.OpenPopup("_TransformGizmoSelection");
ImGui.PopFont();
if (ImGui.BeginPopup("_TransformGizmoSelection"))
{
if (CustomComponents.DrawMenuItem((int)Icon.Move, "Move", isChecked: State.TransformGizmoMode == TransformGizmoModes.Move))
{
State.TransformGizmoMode = TransformGizmoModes.Move;
FlagSymbolUiAsModified();
}
if (CustomComponents.DrawMenuItem((int)Icon.Rotate, "Rotate",
isChecked: State.TransformGizmoMode == TransformGizmoModes.Rotate))
{
State.TransformGizmoMode = TransformGizmoModes.Rotate;
FlagSymbolUiAsModified();
}
if (CustomComponents.DrawMenuItem((int)Icon.Scale, "Scale", isChecked: State.TransformGizmoMode == TransformGizmoModes.Scale))
{
State.TransformGizmoMode = TransformGizmoModes.Scale;
FlagSymbolUiAsModified();
}
ImGui.EndPopup();
}
}
ImGui.SameLine();
_camSelectionHandling.DrawCameraControlSelection();
// Camera speed
{
ImGui.SameLine();
ImGui.PushID("CamSpeed");
SingleValueEdit.Draw(ref UserSettings.Config.CameraSpeed, new Vector2(ImGui.GetFrameHeight() * 2, ImGui.GetFrameHeight()), min: 0.001f,
max: 100,
clampMin: true,
clampMax: true,
scale: 0.01f,
format: " {0:G3}");
Icons.DrawIconOnLastItem(Icon.CameraSpeed,
Math.Abs(UserSettings.Config.CameraSpeed - UserSettings.Defaults.CameraSpeed) < 0.001f
? UiColors.TextMuted
: UiColors.Text, 0.1f);
CustomComponents.TooltipForLastItem("Camera speed when flying with WASD keys.", "TIP: Use mouse wheel while flying to adjust on the fly.");
ImGui.PopID();
}
// the background color button got me confused as it has no effect for Texture2D so I decided to only show it for Command
if (drawnType == typeof(Command))
{
ImGui.SameLine();
ColorEditButton.Draw(ref _backgroundColor, Vector2.Zero);
CustomComponents.TooltipForLastItem("Adjust background color of view");
}
ImGui.SameLine();
ResolutionHandling.DrawSelector(ref _selectedResolution, _resolutionDialog);
// Screenshot and Render
if (RenderProcess.State != RenderProcess.States.NoValidOutputType && RenderProcess.State != RenderProcess.States.NoOutputWindow)
{
//var texture = GetCurrentTexture();
//if (drawnType == typeof(Texture2D) || drawnType == typeof(Command))
//{
ImGui.SameLine(0, 2);
var screenshotState = !RenderProcess.IsExporting && RenderProcess.MainOutputType != null
? CustomComponents.ButtonStates.Emphasized
: CustomComponents.ButtonStates.Disabled;
if (CustomComponents.IconButton(Icon.Snapshot, Vector2.Zero, screenshotState))
{
RenderProcess.TryRenderScreenShot();
}
if(ImGui.IsAnyItemHovered())
CustomComponents.TooltipForLastItem("Save screenshot",
UserActions.RenderScreenshot.ListKeyboardShortcutsForActionWithLabel());
ImGui.SameLine();
var renderAnimState = RenderProcess.IsExporting
? CustomComponents.ButtonStates.NeedsAttention
: RenderProcess.MainOutputType != null
? CustomComponents.ButtonStates.Emphasized
: CustomComponents.ButtonStates.Disabled;
if (CustomComponents.IconButton(Icon.RenderAnimation, Vector2.Zero, renderAnimState))
{
if (RenderProcess.IsExporting)
{
RenderProcess.Cancel();
}
else
{
RenderProcess.TryStartVideoExport();
}
}
if (ImGui.IsAnyItemHovered())
{
CustomComponents.TooltipForLastItem("Render Animation",
BuildRenderSummaryTooltip()
+ UserActions.RenderAnimation.ListKeyboardShortcutsForActionWithLabel());
}
ImGui.SameLine();
var renderSettingsOpen = WindowManager.IsAnyInstanceVisible<RenderWindow>();
var renderSettingsState = renderSettingsOpen
? CustomComponents.ButtonStates.Activated
: CustomComponents.ButtonStates.Emphasized;
if (CustomComponents.IconButton(Icon.Settings2, Vector2.Zero, renderSettingsState))
{
WindowManager.ToggleInstanceVisibility<RenderWindow>();
}
if (ImGui.IsAnyItemHovered())
CustomComponents.TooltipForLastItem("Toggle render settings",
"Open or close the \"Render To File\" window.");
}
CustomComponents.PopToolbarIconBackground();
ImGui.EndChild();
}
// Render-icon tooltip: the range mode, then duration, resolution, format, and rough size / render-time estimates.
private static string BuildRenderSummaryTooltip()
{
var s = RenderSettings.Current;
if (!RenderProcess.TryGetRenderResolution(s, out var res))
return string.Empty;
var mode = s.TimeRange.ToString(); // Custom / Loop / Soundtrack / Continuous
// Continuous has no fixed end, so a duration / size estimate would be meaningless.
if (s.TimeRange == RenderSettings.TimeRanges.Continuous)
{
var clock = s.ContinuousClock == RenderSettings.ContinuousCaptureClock.Realtime ? "realtime" : "deterministic";
if (s.RenderMode == RenderSettings.RenderModes.Video)
{
var (cw, ch) = s.VideoCodec.RoundToEncoderBlock(res.Width, res.Height);
return $"{mode} · {cw}×{ch} · {s.VideoCodec} · {clock} · {s.FrameRate:0} fps\n";
}
return $"{mode} · {res.Width}×{res.Height} · {s.FileFormat} sequence · {clock} · {s.FrameRate:0} fps\n";
}
var frames = RenderTiming.ComputeFrameCount(s);
var dur = System.Math.Max(0, RenderTiming.ReferenceTimeToSeconds(s.EndInBars, s.TimeReference, s.FrameRate)
- RenderTiming.ReferenceTimeToSeconds(s.StartInBars, s.TimeReference, s.FrameRate));
if (s.RenderMode == RenderSettings.RenderModes.Video)
{
var (w, h) = s.VideoCodec.RoundToEncoderBlock(res.Width, res.Height);
var bytes = RenderExportEstimate.EstimateBytes(s.VideoCodec, res, frames, dur, s.Bitrate);
var renderSecs = RenderExportEstimate.EstimateSeconds(s.VideoCodec, res, frames, s.OverrideMotionBlurSamples);
return $"{mode} · {dur / 60:0}:{dur % 60:00}s · {w}×{h} · {s.VideoCodec}\n"
+ $"~{RenderExportEstimate.FormatBytes(bytes)} · {RenderExportEstimate.FormatDuration(renderSecs)} to render\n";
}
return $"{mode} · {dur / 60:0}:{dur % 60:00}s · {res.Width}×{res.Height} · {s.FileFormat} sequence ({frames} frames)\n";
}
private static void DrawRenderProgressBar()
{
if (!RenderProcess.IsExporting) return;
var dl = ImGui.GetForegroundDrawList();
var p = ImGui.GetWindowPos();
var width = ImGui.GetWindowSize().X;
var size = new Vector2(width, 2);
dl.AddRectFilled(p, p + size, UiColors.BackgroundFull.Fade(0.4f));
var progress = RenderProcess.Progress;
if (progress < 0)
{
// Open-ended continuous capture: a sweeping segment instead of a determinate fill.
const float segmentFraction = 0.3f;
var segWidth = width * segmentFraction;
var t = (float)((ImGui.GetTime() * 0.3) % 1.0);
var segStart = p.X - segWidth + t * (width + segWidth);
var segLeft = Math.Max(segStart, p.X);
var segRight = Math.Min(segStart + segWidth, p.X + width);
if (segRight > segLeft)
dl.AddRectFilled(new Vector2(segLeft, p.Y), new Vector2(segRight, p.Y + size.Y), UiColors.StatusAttention);
return;
}
dl.AddRectFilled(p, p + new Vector2(size.X * (float)progress, size.Y), UiColors.StatusAttention);
}
/// <summary>
/// Update content with an <see cref="Core.Operator.EvaluationContext"/> and use the DrawImplementation for the given type to draw it.
/// </summary>
private Type? UpdateAndDrawOutput(Instance? instanceForOutput, Instance?instanceForEvaluation = null)
{
instanceForEvaluation ??= instanceForOutput;
if (instanceForEvaluation == null || instanceForEvaluation.Outputs.Count <= 0)
return null;
var evaluatedSymbolUi = instanceForEvaluation.GetSymbolUi();
var evalOutput = Pinning.GetPinnedOrDefaultOutput(instanceForEvaluation.Outputs);
if (evalOutput == null || !evaluatedSymbolUi.OutputUis.TryGetValue(evalOutput.Id, out var evaluatedOutputUi))
return null;
if (_imageCanvas.ViewMode != ImageOutputCanvas.Modes.Fitted
&& evaluatedOutputUi is CommandOutputUi)
{
_imageCanvas.SetViewMode(ImageOutputCanvas.Modes.Fitted);
}
// Prepare context
EvaluationContext.Reset();
EvaluationContext.ShowGizmos = State.ShowGizmos;
EvaluationContext.TransformGizmoMode = State.TransformGizmoMode;
EvaluationContext.BypassCameras = _camSelectionHandling.BypassCamera;
RequestedResolution = RenderProcess.TryGetActiveExportResolution(out var overrideResolution)
? overrideResolution
: _selectedResolution.ComputeResolution();
EvaluationContext.RequestedResolution = RequestedResolution;
// Set camera
if (_camSelectionHandling.CameraForRendering != null)
{
EvaluationContext.SetViewFromCamera(_camSelectionHandling.CameraForRendering);
}
EvaluationContext.BackgroundColor = _backgroundColor;
const string overrideSampleVariableName = "OverrideMotionBlurSamples";
if (RenderProcess.IsExporting)
{
var samples = RenderProcess.GetActiveOrRequestedSettings().OverrideMotionBlurSamples;
if (samples >= 0)
{
EvaluationContext.IntVariables[overrideSampleVariableName] = samples;
}
}
else
{
EvaluationContext.IntVariables.Remove(overrideSampleVariableName);
}
// Ugly hack to hide final target
if (instanceForOutput != instanceForEvaluation)
{
ImGui.BeginChild("hidden", Vector2.One);
{
evaluatedOutputUi.DrawValue(evalOutput, EvaluationContext, Config.Title);
}
ImGui.EndChild();
if (instanceForOutput == null || instanceForOutput.Outputs.Count == 0)
return null;
var viewOutput = Pinning.GetPinnedOrDefaultOutput(instanceForOutput.Outputs);
var viewSymbolUi = instanceForOutput.GetSymbolUi();
if (viewOutput == null || !viewSymbolUi.OutputUis.TryGetValue(viewOutput.Id, out var viewOutputUi))
return null;
// Render!
viewOutputUi.DrawValue(viewOutput, EvaluationContext, Config.Title, recompute: false);
return viewOutputUi.Type;
}
// Render!
evaluatedOutputUi.DrawValue(evalOutput, EvaluationContext, Config.Title);
return evalOutput.ValueType;
}
public Instance? ShownInstance
{
get
{
Pinning.TryGetPinnedOrSelectedInstance(out var instance, out _);
return instance;
}
}
#region State persistence
/// <summary>
/// Checks if the project root changed, and if so saves the current state
/// to the previous project and loads the new project's state.
/// Uses root instance (not current composition) because output window state is project-global.
/// </summary>
private void SyncStateWithProject()
{
var rootInstance = ProjectView.Focused?.RootInstance;
var symbolUi = rootInstance?.Symbol.GetSymbolUi();
var symbolId = symbolUi?.Symbol.Id ?? Guid.Empty;
if (symbolId == _lastSyncedSymbolId)
return;
// Save current state to the PREVIOUS project before switching
SaveStateToProject();
_lastSyncedSymbolId = symbolId;
_lastSyncedSymbolUi = symbolUi;
if (symbolUi == null)
return;
// Load state for new project
var instanceIndex = OutputWindowInstances.IndexOf(this);
if (instanceIndex < 0)
return;
if (symbolUi.OutputWindowStates is { } states && instanceIndex < states.Count)
{
LoadStateFrom(states[instanceIndex]);
}
}
internal void SaveStateToProject()
{
var symbolUi = _lastSyncedSymbolUi;
if (symbolUi == null)
return;
var instanceIndex = OutputWindowInstances.IndexOf(this);
if (instanceIndex < 0)
return;
symbolUi.OutputWindowStates ??= [];
// Grow the list if needed
while (symbolUi.OutputWindowStates.Count <= instanceIndex)
symbolUi.OutputWindowStates.Add(new OutputWindowState());
var state = symbolUi.OutputWindowStates[instanceIndex];
SaveStateTo(state);
FlagSymbolUiAsModified();
}
/// <summary>
/// Returns the persisted state for this output window instance, creating it if needed.
/// ShowGizmos and TransformGizmoMode are read/written directly from this object.
/// </summary>
internal OutputWindowState State
{
get
{
var symbolUi = _lastSyncedSymbolUi;
if (symbolUi == null)
return _fallbackState;
var instanceIndex = OutputWindowInstances.IndexOf(this);
if (instanceIndex < 0)
return _fallbackState;
symbolUi.OutputWindowStates ??= [];
while (symbolUi.OutputWindowStates.Count <= instanceIndex)
symbolUi.OutputWindowStates.Add(new OutputWindowState());
return symbolUi.OutputWindowStates[instanceIndex];
}
}
private readonly OutputWindowState _fallbackState = new();
/// <summary>
/// Read-only symbols keep view-state changes in memory but must not be flagged as modified —
/// merely viewing them would prompt to save them as a copy.
/// </summary>
private void FlagSymbolUiAsModified()
{
if (_lastSyncedSymbolUi is { ReadOnly: false })
{
_lastSyncedSymbolUi.FlagAsModified();
}
}
/// <summary>
/// Syncs fields that are still owned by OutputWindow components (not yet direct-backed by State)
/// to the State object every frame. This ensures saves always capture current values.
/// </summary>
private void SyncCopyFieldsToState()
{
if (_lastSyncedSymbolUi == null)
return;
var state = State;
if (state == _fallbackState)
return;
state.BackgroundColor = [_backgroundColor.X, _backgroundColor.Y, _backgroundColor.Z, _backgroundColor.W];
state.ResolutionTitle = _selectedResolution.Title;
state.ResolutionWidth = _selectedResolution.Size.Width;
state.ResolutionHeight = _selectedResolution.Size.Height;
state.ResolutionUseAsAspectRatio = _selectedResolution.UseAsAspectRatio;
state.CameraSpeed = UserSettings.Config.CameraSpeed;
_camSelectionHandling.SaveStateTo(state);
Pinning.SaveStateTo(state);
}
private void SaveStateTo(OutputWindowState state)
{
// ShowGizmos and TransformGizmoMode are already on the state object (direct backing store)
state.BackgroundColor = [_backgroundColor.X, _backgroundColor.Y, _backgroundColor.Z, _backgroundColor.W];
state.CameraSpeed = UserSettings.Config.CameraSpeed;
// Resolution
state.ResolutionTitle = _selectedResolution.Title;
state.ResolutionWidth = _selectedResolution.Size.Width;
state.ResolutionHeight = _selectedResolution.Size.Height;
state.ResolutionUseAsAspectRatio = _selectedResolution.UseAsAspectRatio;
_camSelectionHandling.SaveStateTo(state);
Pinning.SaveStateTo(state);
}
private void LoadStateFrom(OutputWindowState state)
{
// ShowGizmos and TransformGizmoMode are read directly from state — no copy needed
if (state.BackgroundColor.Length == 4)
_backgroundColor = new System.Numerics.Vector4(state.BackgroundColor[0], state.BackgroundColor[1], state.BackgroundColor[2], state.BackgroundColor[3]);
UserSettings.Config.CameraSpeed = state.CameraSpeed;
// Resolution — try to find by title first, fall back to size
var resolution = ResolutionHandling.FindByTitle(state.ResolutionTitle)
?? new ResolutionHandling.Resolution(
state.ResolutionTitle ?? "Custom",
state.ResolutionWidth,
state.ResolutionHeight,
state.ResolutionUseAsAspectRatio);
_selectedResolution = resolution;
_camSelectionHandling.LoadStateFrom(state);
Pinning.LoadStateFrom(state);
}
private Guid _lastSyncedSymbolId;
private SymbolUi? _lastSyncedSymbolUi;
#endregion
public static readonly List<Window> OutputWindowInstances = [];
public ViewSelectionPinning Pinning { get; } = new();
private System.Numerics.Vector4 _backgroundColor = new(0.1f, 0.1f, 0.1f, 1.0f);
internal readonly EvaluationContext EvaluationContext = new();
private readonly ImageOutputCanvas _imageCanvas = new();
private readonly CameraSelectionHandling _camSelectionHandling;
private static int _instanceCounter;
private ResolutionHandling.Resolution _selectedResolution = ResolutionHandling.DefaultResolution;
internal Int2 RequestedResolution { get; private set; }
private readonly EditResolutionDialog _resolutionDialog = new();
}