From 13a16a1830d15d7500299ba30c84da052eb7a6ef Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 4 Sep 2021 20:52:03 +0200 Subject: [PATCH] UI - Performance improvements --- .../Controls/DeviceVisualizer.cs | 119 ++++++++++-------- .../Services/ProfileEditorService.cs | 8 +- .../Utilities/HitTestUtilities.cs | 3 +- .../LayerPropertiesViewModel.cs | 38 +++--- .../Visualization/ProfileLayerViewModel.cs | 89 +++++++------ .../Visualization/Tools/EditToolViewModel.cs | 2 +- .../Screens/Shared/PanZoomViewModel.cs | 16 ++- .../SurfaceEditor/SurfaceEditorView.xaml | 3 +- .../SurfaceEditor/SurfaceEditorViewModel.cs | 22 ++-- 9 files changed, 165 insertions(+), 135 deletions(-) diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 5c9ea5812..427ad1808 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; @@ -9,15 +11,15 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; +using System.Windows.Threading; using Artemis.Core; -using Stylet; namespace Artemis.UI.Shared { /// /// Visualizes an with optional per-LED colors /// - public class DeviceVisualizer : FrameworkElement, IDisposable + public class DeviceVisualizer : FrameworkElement { /// /// The device to visualize @@ -34,14 +36,16 @@ namespace Artemis.UI.Shared /// /// A list of LEDs to highlight /// - public static readonly DependencyProperty HighlightedLedsProperty = DependencyProperty.Register(nameof(HighlightedLeds), typeof(IEnumerable), typeof(DeviceVisualizer), - new FrameworkPropertyMetadata(default(IEnumerable))); + public static readonly DependencyProperty HighlightedLedsProperty = DependencyProperty.Register(nameof(HighlightedLeds), typeof(ObservableCollection), typeof(DeviceVisualizer), + new FrameworkPropertyMetadata(default(ObservableCollection), HighlightedLedsPropertyChanged)); private readonly DrawingGroup _backingStore; private readonly List _deviceVisualizerLeds; - private readonly Timer _timer; + private readonly DispatcherTimer _timer; private BitmapImage? _deviceImage; private ArtemisDevice? _oldDevice; + private List _highlightedLeds; + private List _dimmedLeds; /// /// Creates a new instance of the class @@ -50,9 +54,10 @@ namespace Artemis.UI.Shared { _backingStore = new DrawingGroup(); _deviceVisualizerLeds = new List(); + _dimmedLeds = new List(); // Run an update timer at 25 fps - _timer = new Timer(40); + _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)}; MouseLeftButtonUp += OnMouseLeftButtonUp; Loaded += OnLoaded; @@ -80,9 +85,9 @@ namespace Artemis.UI.Shared /// /// Gets or sets a list of LEDs to highlight /// - public IEnumerable? HighlightedLeds + public ObservableCollection? HighlightedLeds { - get => (IEnumerable) GetValue(HighlightedLedsProperty); + get => (ObservableCollection) GetValue(HighlightedLedsProperty); set => SetValue(HighlightedLedsProperty, value); } @@ -158,10 +163,9 @@ namespace Artemis.UI.Shared protected virtual void Dispose(bool disposing) { if (disposing) - _timer.Dispose(); + _timer.Stop(); } - private static Size ResizeKeepAspect(Size src, double maxWidth, double maxHeight) { double scale; @@ -191,8 +195,10 @@ namespace Artemis.UI.Shared private void OnUnloaded(object? sender, RoutedEventArgs e) { _timer.Stop(); - _timer.Elapsed -= TimerOnTick; + _timer.Tick -= TimerOnTick; + if (HighlightedLeds != null) + HighlightedLeds.CollectionChanged -= HighlightedLedsChanged; if (_oldDevice != null) { if (Device != null) @@ -223,16 +229,34 @@ namespace Artemis.UI.Shared private void OnLoaded(object? sender, RoutedEventArgs e) { _timer.Start(); - _timer.Elapsed += TimerOnTick; + _timer.Tick += TimerOnTick; } private void TimerOnTick(object? sender, EventArgs e) { - Execute.PostToUIThread(() => + if (ShowColors && Visibility == Visibility.Visible) + Render(); + } + + private void Render() + { + DrawingContext drawingContext = _backingStore.Append(); + + if (_highlightedLeds.Any()) { - if (ShowColors && Visibility == Visibility.Visible) - Render(); - }); + foreach (DeviceVisualizerLed deviceVisualizerLed in _highlightedLeds) + deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false); + + foreach (DeviceVisualizerLed deviceVisualizerLed in _dimmedLeds) + deviceVisualizerLed.RenderColor(_backingStore, drawingContext, true); + } + else + { + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false); + } + + drawingContext.Close(); } private void UpdateTransform() @@ -241,22 +265,12 @@ namespace Artemis.UI.Shared InvalidateMeasure(); } - private static void DevicePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; - deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); }); - } - - private static void ShowColorsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; - deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); }); - } - private void SetupForDevice() { _deviceImage = null; _deviceVisualizerLeds.Clear(); + _highlightedLeds = new List(); + _dimmedLeds = new List(); if (Device == null) return; @@ -319,40 +333,47 @@ namespace Artemis.UI.Shared private void DeviceUpdated(object? sender, EventArgs e) { - Execute.PostToUIThread(SetupForDevice); + Dispatcher.Invoke(SetupForDevice); } private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) { - Execute.PostToUIThread(SetupForDevice); + Dispatcher.Invoke(SetupForDevice); } - private void Render() + private static void DevicePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { - DrawingContext drawingContext = _backingStore.Append(); + DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; + deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); }); + } - if (HighlightedLeds != null && HighlightedLeds.Any()) + private static void ShowColorsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; + deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); }); + } + + private static void HighlightedLedsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; + if (e.OldValue is ObservableCollection oldCollection) + oldCollection.CollectionChanged -= deviceVisualizer.HighlightedLedsChanged; + if (e.NewValue is ObservableCollection newCollection) + newCollection.CollectionChanged += deviceVisualizer.HighlightedLedsChanged; + } + + private void HighlightedLedsChanged(object? sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + if (HighlightedLeds != null) { - // Faster on large devices, maybe a bit slower on smaller ones but that's ok - ILookup toHighlight = HighlightedLeds.ToLookup(l => l); - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderColor(_backingStore, drawingContext, !toHighlight.Contains(deviceVisualizerLed.Led)); + _highlightedLeds = _deviceVisualizerLeds.Where(l => HighlightedLeds.Contains(l.Led)).ToList(); + _dimmedLeds = _deviceVisualizerLeds.Where(l => !HighlightedLeds.Contains(l.Led)).ToList(); } else { - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false); + _highlightedLeds = new List(); + _dimmedLeds = new List(); } - - drawingContext.Close(); - } - - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index f3f3626ea..282d63116 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -12,7 +12,6 @@ using Ninject.Parameters; using Serilog; using SkiaSharp; using SkiaSharp.Views.WPF; -using Stylet; namespace Artemis.UI.Shared.Services { @@ -45,9 +44,11 @@ namespace Artemis.UI.Shared.Services private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) { - if (!_doTick) return; + if (!_doTick) + return; _doTick = false; - Execute.PostToUIThread(OnProfilePreviewUpdated); + + OnProfilePreviewUpdated(); } private void ReloadProfile() @@ -144,6 +145,7 @@ namespace Artemis.UI.Shared.Services { if (_currentTime.Equals(value)) return; _currentTime = value; + Tick(); OnCurrentTimeChanged(); } diff --git a/src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs b/src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs index 5a76bde26..7d1c99254 100644 --- a/src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs +++ b/src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs @@ -25,7 +25,8 @@ namespace Artemis.UI.Shared HitTestResultBehavior ResultCallback(HitTestResult r) => HitTestResultBehavior.Continue; HitTestFilterBehavior FilterCallback(DependencyObject e) { - if (e is FrameworkElement fe && fe.DataContext is T context && !result.Contains(context)) result.Add(context); + if (e is FrameworkElement fe && fe.DataContext is T context && !result.Contains(context)) + result.Add(context); return HitTestFilterBehavior.Continue; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 2b748fc01..95af67ace 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -583,30 +583,28 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties return; } - Execute.PostToUIThread(() => + TimeSpan newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime)); + if (SelectedProfileElement != null) { - TimeSpan newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime)); - if (SelectedProfileElement != null) + if (Repeating && RepeatTimeline) { - if (Repeating && RepeatTimeline) - { - if (newTime > SelectedProfileElement.Timeline.Length) - newTime = TimeSpan.Zero; - } - else if (Repeating && RepeatSegment) - { - if (newTime > GetCurrentSegmentEnd()) - newTime = GetCurrentSegmentStart(); - } - else if (newTime > SelectedProfileElement.Timeline.Length) - { - newTime = SelectedProfileElement.Timeline.Length; - Pause(); - } + if (newTime > SelectedProfileElement.Timeline.Length) + newTime = TimeSpan.Zero; } + else if (Repeating && RepeatSegment) + { + if (newTime > GetCurrentSegmentEnd()) + newTime = GetCurrentSegmentStart(); + } + else if (newTime > SelectedProfileElement.Timeline.Length) + { + newTime = SelectedProfileElement.Timeline.Length; + Pause(); + } + } - ProfileEditorService.CurrentTime = newTime; - }); + // Update current time on high priority to keep things buttery smooth as if you're using the mouse ༼ つ ◕_◕ ༽つ + Execute.OnUIThreadSync(() => ProfileEditorService.CurrentTime = newTime); } #endregion diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs index 85f01fd94..f37a9d081 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs @@ -8,6 +8,8 @@ using Artemis.UI.Extensions; using Artemis.UI.Screens.Shared; using Artemis.UI.Services; using Artemis.UI.Shared.Services; +using SkiaSharp; +using Stylet; namespace Artemis.UI.Screens.ProfileEditor.Visualization { @@ -95,6 +97,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization CreateViewportRectangle(); } + #region Updating + + private LayerShape _lastShape; + private Rect _lastBounds; + + private SKPoint _lastAnchor; + private SKPoint _lastPosition; + private double _lastRotation; + private SKSize _lastScale; + private void CreateShapeGeometry() { if (Layer.LayerShape == null || !Layer.Leds.Any()) @@ -104,21 +116,22 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization } Rect bounds = _layerEditorService.GetLayerBounds(Layer); - Geometry shapeGeometry = Geometry.Empty; - switch (Layer.LayerShape) + + // Only bother the UI thread for both of these if we're sure + if (HasShapeChanged(bounds)) { - case EllipseShape _: - shapeGeometry = new EllipseGeometry(bounds); - break; - case RectangleShape _: - shapeGeometry = new RectangleGeometry(bounds); - break; + Execute.OnUIThreadSync(() => + { + if (Layer.LayerShape is RectangleShape) + ShapeGeometry = new RectangleGeometry(bounds); + if (Layer.LayerShape is EllipseShape) + ShapeGeometry = new EllipseGeometry(bounds); + }); + } + if ((Layer.LayerBrush == null || Layer.LayerBrush.SupportsTransformation) && HasTransformationChanged()) + { + Execute.OnUIThreadSync(() => ShapeGeometry.Transform = _layerEditorService.GetLayerTransformGroup(Layer)); } - - if (Layer.LayerBrush == null || Layer.LayerBrush.SupportsTransformation) - shapeGeometry.Transform = _layerEditorService.GetLayerTransformGroup(Layer); - - ShapeGeometry = shapeGeometry; } private void CreateViewportRectangle() @@ -132,44 +145,30 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization ViewportRectangle = _layerEditorService.GetLayerBounds(Layer); } - private Geometry CreateRectangleGeometry(ArtemisLed led) + private bool HasShapeChanged(Rect bounds) { - Rect rect = led.RgbLed.AbsoluteBoundary.ToWindowsRect(1); - return new RectangleGeometry(rect); + bool result = !Equals(_lastBounds, bounds) || !Equals(_lastShape, Layer.LayerShape); + _lastShape = Layer.LayerShape; + _lastBounds = bounds; + return result; } - private Geometry CreateCircleGeometry(ArtemisLed led) + private bool HasTransformationChanged() { - Rect rect = led.RgbLed.AbsoluteBoundary.ToWindowsRect(1); - return new EllipseGeometry(rect); + bool result = _lastAnchor != Layer.Transform.AnchorPoint.CurrentValue || + _lastPosition != Layer.Transform.Position.CurrentValue || + _lastRotation != Layer.Transform.Rotation.CurrentValue || + _lastScale != Layer.Transform.Scale.CurrentValue; + + _lastAnchor = Layer.Transform.AnchorPoint.CurrentValue; + _lastPosition = Layer.Transform.Position.CurrentValue; + _lastRotation = Layer.Transform.Rotation.CurrentValue; + _lastScale = Layer.Transform.Scale.CurrentValue; + + return result; } - private Geometry CreateCustomGeometry(ArtemisLed led, double deflateAmount) - { - Rect rect = led.RgbLed.AbsoluteBoundary.ToWindowsRect(1); - try - { - PathGeometry geometry = Geometry.Combine( - Geometry.Empty, - Geometry.Parse(led.RgbLed.ShapeData), - GeometryCombineMode.Union, - new TransformGroup - { - Children = new TransformCollection - { - new ScaleTransform(rect.Width, rect.Height), - new TranslateTransform(rect.X, rect.Y) - } - } - ); - - return geometry; - } - catch (Exception) - { - return CreateRectangleGeometry(led); - } - } + #endregion #region Overrides of Screen diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs index 8e85a3b1f..3f0e8fbde 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs @@ -51,7 +51,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools private void Update() { - if (!(ProfileEditorService.SelectedProfileElement is Layer layer)) + if (ProfileEditorService.SelectedProfileElement is not Layer layer) return; ShapePath = _layerEditorService.GetLayerPath(layer, true, true, true); diff --git a/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs b/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs index d1e3d88fe..c7e241fa8 100644 --- a/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs +++ b/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs @@ -152,11 +152,6 @@ namespace Artemis.UI.Screens.Shared PanY = rect.Top * -1 * Zoom; } - public Rect TransformContainingRect(Rectangle rect) - { - return TransformContainingRect(rect.ToWindowsRect(1)); - } - public Rect TransformContainingRect(Rect rect) { // Create the same transform group the view is using @@ -168,6 +163,17 @@ namespace Artemis.UI.Screens.Shared return transformGroup.TransformBounds(rect); } + public Rect UnTransformContainingRect(Rect rect) + { + // Create the same transform group the view is using + TransformGroup transformGroup = new(); + transformGroup.Children.Add(new TranslateTransform(PanX * -1, PanY * -1)); + transformGroup.Children.Add(new ScaleTransform(1 / Zoom, 1 / Zoom)); + + // Apply it to the device rect + return transformGroup.TransformBounds(rect); + } + public Point GetRelativeMousePosition(object container, MouseEventArgs e) { // Get the mouse position relative to the panned / zoomed panel, not very MVVM but I can't find a better way diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml index d569d59d4..c4fed7122 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml @@ -188,8 +188,7 @@ - devices = HitTestUtilities.GetHitViewModels((Visual) sender, selectedRect); + SKRect hitTestRect = PanZoomViewModel.UnTransformContainingRect(new Rect(_mouseDragStartPoint, position)).ToSKRect(); foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels) - if (devices.Contains(device)) + { + if (device.Device.Rectangle.IntersectsWith(hitTestRect)) device.SelectionStatus = SelectionStatus.Selected; else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) device.SelectionStatus = SelectionStatus.None; + } } else { @@ -382,7 +384,7 @@ namespace Artemis.UI.Screens.SurfaceEditor ApplySurfaceSelection(); } - private void UpdateSelection(object sender, Point position) + private void UpdateSelection(Point position) { if (IsPanKeyDown()) return; @@ -390,12 +392,14 @@ namespace Artemis.UI.Screens.SurfaceEditor Rect selectedRect = new(_mouseDragStartPoint, position); SelectionRectangle.Rect = selectedRect; - List devices = HitTestUtilities.GetHitViewModels((Visual) sender, SelectionRectangle); + SKRect hitTestRect = PanZoomViewModel.UnTransformContainingRect(new Rect(_mouseDragStartPoint, position)).ToSKRect(); foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels) - if (devices.Contains(device)) + { + if (device.Device.Rectangle.IntersectsWith(hitTestRect)) device.SelectionStatus = SelectionStatus.Selected; else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) device.SelectionStatus = SelectionStatus.None; + } ApplySurfaceSelection(); }