1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Implemented keyframe easing

This commit is contained in:
SpoinkyNL 2020-01-12 23:49:10 +01:00
parent bda77b12f0
commit 0ff71c9d3b
33 changed files with 651 additions and 164 deletions

View File

@ -15,7 +15,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
var nextKeyframe = (Keyframe<float>) NextKeyframe;
var diff = nextKeyframe.Value - currentKeyframe.Value;
return currentKeyframe.Value + diff * KeyframeProgress;
return currentKeyframe.Value + diff * KeyframeProgressEased;
}
}
}

View File

@ -15,7 +15,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
var nextKeyframe = (Keyframe<int>) NextKeyframe;
var diff = nextKeyframe.Value - currentKeyframe.Value;
return currentKeyframe.Value + diff * KeyframeProgress;
return currentKeyframe.Value + diff * KeyframeProgressEased;
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Exceptions;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Utilities;
namespace Artemis.Core.Models.Profile.KeyframeEngines
{
@ -24,10 +25,17 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
public TimeSpan Progress { get; private set; }
/// <summary>
/// The progress from the current keyframe to the next 0 to 1
/// The progress from the current keyframe to the next.
/// <para>Range 0.0 to 1.0.</para>
/// </summary>
public float KeyframeProgress { get; private set; }
/// <summary>
/// The progress from the current keyframe to the next with the current keyframes easing function applied.
/// <para>Range 0.0 to 1.0 but can be higher than 1.0 depending on easing function.</para>
/// </summary>
public float KeyframeProgressEased { get; set; }
/// <summary>
/// The current keyframe
/// </summary>
@ -68,28 +76,35 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
if (!Initialized)
return;
var keyframes = LayerProperty.UntypedKeyframes.ToList();
Progress = Progress.Add(TimeSpan.FromMilliseconds(deltaTime));
// TODO Keep them sorted somewhere else, iterating all keyframes multiple times sucks
var sortedKeyframes = LayerProperty.UntypedKeyframes.ToList().OrderBy(k => k.Position).ToList();
// The current keyframe is the last keyframe before the current time
CurrentKeyframe = keyframes.LastOrDefault(k => k.Position <= Progress);
// The next keyframe is the first keyframe that's after the current time
NextKeyframe = keyframes.FirstOrDefault(k => k.Position > Progress);
CurrentKeyframe = sortedKeyframes.LastOrDefault(k => k.Position <= Progress);
NextKeyframe = sortedKeyframes.FirstOrDefault(k => k.Position > Progress);
if (CurrentKeyframe == null)
{
KeyframeProgress = 0;
KeyframeProgressEased = 0;
}
else if (NextKeyframe == null)
{
KeyframeProgress = 1;
KeyframeProgressEased = 1;
}
else
{
var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position;
KeyframeProgress = (float) ((Progress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds);
KeyframeProgressEased = (float) Easings.Interpolate(KeyframeProgress, CurrentKeyframe.EasingFunction);
}
// TODO Apply easing and store it separately
// LayerProperty determines what's next: reset, stop, continue
}
/// <summary>
/// Overrides the engine's progress to the provided value
/// </summary>
@ -106,6 +121,8 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
/// <returns></returns>
public object GetCurrentValue()
{
if (CurrentKeyframe == null && LayerProperty.UntypedKeyframes.Any())
return LayerProperty.UntypedKeyframes.First().BaseValue;
if (CurrentKeyframe == null)
return LayerProperty.BaseValue;
if (NextKeyframe == null)

View File

@ -17,7 +17,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
var xDiff = nextKeyframe.Value.X - currentKeyframe.Value.X;
var yDiff = nextKeyframe.Value.Y - currentKeyframe.Value.Y;
return new SKPoint(currentKeyframe.Value.X + xDiff * KeyframeProgress, currentKeyframe.Value.Y + yDiff * KeyframeProgress);
return new SKPoint(currentKeyframe.Value.X + xDiff * KeyframeProgressEased, currentKeyframe.Value.Y + yDiff * KeyframeProgressEased);
}
}
}

View File

@ -17,7 +17,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
var widthDiff = nextKeyframe.Value.Width - currentKeyframe.Value.Width;
var heightDiff = nextKeyframe.Value.Height - currentKeyframe.Value.Height;
return new SKSize(currentKeyframe.Value.Width + widthDiff * KeyframeProgress, currentKeyframe.Value.Height + heightDiff * KeyframeProgress);
return new SKSize(currentKeyframe.Value.Width + widthDiff * KeyframeProgressEased, currentKeyframe.Value.Height + heightDiff * KeyframeProgressEased);
}
}
}

View File

@ -48,7 +48,6 @@ namespace Artemis.Core.Models.Profile
_leds = new List<ArtemisLed>();
_properties = new Dictionary<string, BaseLayerProperty>();
// TODO: Load properties from entity instead of creating the defaults
CreateDefaultProperties();
switch (layerEntity.ShapeEntity?.Type)
@ -355,7 +354,7 @@ namespace Artemis.Core.Models.Profile
private void CreateDefaultProperties()
{
var transformProperty = new LayerProperty<object>(this, null, "Core.Transform", "Transform", "The default properties collection every layer has, allows you to transform the shape.");
var transformProperty = new LayerProperty<object>(this, null, "Core.Transform", "Transform", "The default properties collection every layer has, allows you to transform the shape.") {ExpandByDefault = true};
AnchorPointProperty = new LayerProperty<SKPoint>(this, transformProperty, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position.");
PositionProperty = new LayerProperty<SKPoint>(this, transformProperty, "Core.Position", "Position", "The position of the shape.");
SizeProperty = new LayerProperty<SKSize>(this, transformProperty, "Core.Size", "Size", "The size of the shape.") {InputAffix = "%"};

View File

@ -1,9 +1,12 @@
using System;
using Artemis.Core.Utilities;
namespace Artemis.Core.Models.Profile.LayerProperties
{
public class BaseKeyframe
{
private TimeSpan _position;
protected BaseKeyframe(Layer layer, BaseLayerProperty property)
{
Layer = layer;
@ -11,9 +14,20 @@ namespace Artemis.Core.Models.Profile.LayerProperties
}
public Layer Layer { get; set; }
public TimeSpan Position { get; set; }
public TimeSpan Position
{
get => _position;
set
{
if (value == _position) return;
_position = value;
BaseProperty.SortKeyframes();
}
}
protected BaseLayerProperty BaseProperty { get; }
protected internal object BaseValue { get; set; }
public Easings.Functions EasingFunction { get; set; }
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Exceptions;
using Artemis.Core.Models.Profile.KeyframeEngines;
using Artemis.Core.Utilities;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
@ -56,6 +57,11 @@ namespace Artemis.Core.Models.Profile.LayerProperties
/// </summary>
public string Description { get; set; }
/// <summary>
/// Whether to expand this property by default, this is useful for important parent properties.
/// </summary>
public bool ExpandByDefault { get; set; }
/// <summary>
/// An optional input prefix to show before input elements in the UI.
/// </summary>
@ -114,7 +120,8 @@ namespace Artemis.Core.Models.Profile.LayerProperties
propertyEntity.KeyframeEntities.Add(new KeyframeEntity
{
Position = baseKeyframe.Position,
Value = JsonConvert.SerializeObject(baseKeyframe.BaseValue)
Value = JsonConvert.SerializeObject(baseKeyframe.BaseValue),
EasingFunction = (int) baseKeyframe.EasingFunction
});
}
}
@ -124,12 +131,15 @@ namespace Artemis.Core.Models.Profile.LayerProperties
BaseValue = DeserializePropertyValue(propertyEntity.Value);
BaseKeyframes.Clear();
foreach (var keyframeEntity in propertyEntity.KeyframeEntities)
foreach (var keyframeEntity in propertyEntity.KeyframeEntities.OrderBy(e => e.Position))
{
// Create a strongly typed keyframe or else it cannot be cast later on
var keyframeType = typeof(Keyframe<>);
var keyframe = (BaseKeyframe) Activator.CreateInstance(keyframeType.MakeGenericType(Type), Layer, this);
keyframe.Position = keyframeEntity.Position;
keyframe.BaseValue = DeserializePropertyValue(keyframeEntity.Value);
keyframe.EasingFunction = (Easings.Functions) keyframeEntity.EasingFunction;
BaseKeyframes.Add(keyframe);
}
}
@ -146,18 +156,25 @@ namespace Artemis.Core.Models.Profile.LayerProperties
keyframe.Position = position;
keyframe.BaseValue = BaseValue;
BaseKeyframes.Add(keyframe);
SortKeyframes();
return keyframe;
}
/// <summary>
/// Removes all keyframes from the property.
/// Removes all keyframes from the property and sets the base value to the current value.
/// </summary>
public void ClearKeyframes()
{
BaseValue = KeyframeEngine.GetCurrentValue();
BaseKeyframes.Clear();
}
internal void SortKeyframes()
{
BaseKeyframes = BaseKeyframes.OrderBy(k => k.Position).ToList();
}
public override string ToString()
{
return $"{nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Description)}: {Description}";

View File

@ -36,6 +36,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties
public void AddKeyframe(Keyframe<T> keyframe)
{
BaseKeyframes.Add(keyframe);
SortKeyframes();
}
/// <summary>
@ -45,6 +46,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties
public void RemoveKeyframe(Keyframe<T> keyframe)
{
BaseKeyframes.Remove(keyframe);
SortKeyframes();
}
/// <summary>

View File

@ -21,5 +21,6 @@ namespace Artemis.Storage.Entities.Profile
{
public TimeSpan Position { get; set; }
public string Value { get; set; }
public int EasingFunction { get; set; }
}
}

View File

@ -60,6 +60,12 @@
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<!-- Disable tab stop/focusable on all content controls -->
<Style TargetType="ContentControl">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -144,6 +144,7 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Behaviors\InputBindingBehavior.cs" />
<Compile Include="Bootstrapper.cs" />
<Compile Include="Converters\ColorToDrawingColorConverter.cs" />
<Compile Include="Converters\ColorToSolidColorBrushConverter.cs" />
@ -171,6 +172,7 @@
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\PropertyTree\PropertyTreeItemViewModel.cs" />
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\PropertyTree\PropertyTreeParentViewModel.cs" />
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\PropertyTree\PropertyTreeViewModel.cs" />
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\Timeline\PropertyTrackEasingViewModel.cs" />
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\Timeline\PropertyTrackKeyframeViewModel.cs" />
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\Timeline\PropertyTimelineHeader.cs" />
<Compile Include="Screens\Module\ProfileEditor\LayerProperties\Timeline\PropertyTrackViewModel.cs" />

View File

@ -0,0 +1,43 @@
using System.Windows;
namespace Artemis.UI.Behaviors
{
public class InputBindingBehavior
{
public static readonly DependencyProperty PropagateInputBindingsToWindowProperty =
DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior),
new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged));
public static bool GetPropagateInputBindingsToWindow(FrameworkElement obj)
{
return (bool) obj.GetValue(PropagateInputBindingsToWindowProperty);
}
public static void SetPropagateInputBindingsToWindow(FrameworkElement obj, bool value)
{
obj.SetValue(PropagateInputBindingsToWindowProperty, value);
}
private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement) d).Loaded += OnLoaded;
}
private static void OnLoaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
frameworkElement.Loaded -= OnLoaded;
var window = Window.GetWindow(frameworkElement);
if (window == null) return;
// Move input bindings from the FrameworkElement to the window.
for (var i = frameworkElement.InputBindings.Count - 1; i >= 0; i--)
{
var inputBinding = frameworkElement.InputBindings[i];
window.InputBindings.Add(inputBinding);
frameworkElement.InputBindings.Remove(inputBinding);
}
}
}
}

View File

@ -62,4 +62,14 @@ namespace Artemis.UI.Ninject.Factories
{
PropertyTimelineViewModel Create(LayerPropertiesViewModel layerPropertiesViewModel);
}
public interface IPropertyTrackViewModelFactory : IViewModelFactory
{
PropertyTrackViewModel Create(PropertyTimelineViewModel propertyTimelineViewModel, LayerPropertyViewModel layerPropertyViewModel);
}
public interface IPropertyTrackKeyframeViewModelFactory : IViewModelFactory
{
PropertyTrackKeyframeViewModel Create(BaseKeyframe keyframe);
}
}

View File

@ -6,10 +6,20 @@
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:LayerPropertiesViewModel}">
d:DataContext="{d:DesignInstance local:LayerPropertiesViewModel}"
behaviors:InputBindingBehavior.PropagateInputBindingsToWindow="True">
<UserControl.InputBindings>
<KeyBinding Command="{s:Action Play}" Key="Space"></KeyBinding>
<KeyBinding Command="{s:Action PlayFromStart}" Modifiers="Shift" Key="Space"></KeyBinding>
</UserControl.InputBindings>
<UserControl.Resources>
<s:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</UserControl.Resources>
<Grid x:Name="ContainerGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
@ -28,9 +38,34 @@
</Grid.RowDefinitions>
<!-- Misc controls & time display -->
<StackPanel Grid.Row="0" VerticalAlignment="Center">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding FormattedCurrentTime}" HorizontalAlignment="Right" Margin="0 0 20 0" />
</StackPanel>
<DockPanel Grid.Row="0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Play from start (Shift+Space)" Command="{s:Action PlayFromStart}">
<materialDesign:PackIcon Kind="StepForward" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Toggle play/pause (Space)" Command="{s:Action Play}">
<StackPanel>
<materialDesign:PackIcon Kind="Play" Visibility="{Binding Playing, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}" />
<materialDesign:PackIcon Kind="Pause" Visibility="{Binding Playing, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Go to start" Command="{s:Action GoToStart}">
<materialDesign:PackIcon Kind="SkipBackward" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Go to end" Command="{s:Action GoToEnd}">
<materialDesign:PackIcon Kind="SkipForward" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Previous frame" Command="{s:Action GoToPreviousFrame}">
<materialDesign:PackIcon Kind="SkipPrevious" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Next frame" Command="{s:Action GoToNextFrame}">
<materialDesign:PackIcon Kind="SkipNext" />
</Button>
</StackPanel>
<StackPanel VerticalAlignment="Center">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding FormattedCurrentTime}" HorizontalAlignment="Right" Margin="0 0 20 0" />
</StackPanel>
</DockPanel>
<!-- Properties tree -->
<ScrollViewer Grid.Row="1" x:Name="PropertyTreeScrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
@ -50,15 +85,14 @@
<!-- Timeline header -->
<ScrollViewer Grid.Row="0" x:Name="TimelineHeaderScrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" ScrollChanged="TimelineScrollChanged">
<Grid MouseDown="{s:Action TimelineMouseDown}"
MouseMove="{s:Action TimelineMouseMove}"
Background="{DynamicResource MaterialDesignCardBackground}">
<Grid Background="{DynamicResource MaterialDesignCardBackground}">
<!-- Caret -->
<Canvas ZIndex="1"
Margin="{Binding TimeCaretPosition}"
Cursor="SizeWE"
MouseEnter="{s:Action CaretMouseEnter}"
MouseLeave="{s:Action CaretMouseLeave}">
MouseDown="{s:Action TimelineMouseDown}"
MouseUp="{s:Action TimelineMouseUp}"
MouseMove="{s:Action TimelineMouseMove}">
<Polygon Points="-10,0 0,20, 10,00" Fill="{StaticResource SecondaryAccentBrush}" />
<Line X1="0" X2="0" Y1="0" Y2="{Binding ActualHeight, ElementName=ContainerGrid}" StrokeThickness="2" Stroke="{StaticResource SecondaryAccentBrush}" />
</Canvas>
@ -74,13 +108,13 @@
<!-- Timeline rails -->
<ScrollViewer Grid.Row="1" x:Name="TimelineRailsScrollViewer" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Hidden" ScrollChanged="TimelineScrollChanged">
<Grid MouseDown="{s:Action TimelineMouseDown}"
MouseMove="{s:Action TimelineMouseMove}">
<Grid>
<Canvas ZIndex="1"
Margin="{Binding TimeCaretPosition}"
Cursor="SizeWE"
MouseEnter="{s:Action CaretMouseEnter}"
MouseLeave="{s:Action CaretMouseLeave}">
MouseDown="{s:Action TimelineMouseDown}"
MouseUp="{s:Action TimelineMouseUp}"
MouseMove="{s:Action TimelineMouseMove}">
<Line X1="0" X2="0" Y1="0" Y2="{Binding ActualHeight, ElementName=ContainerGrid}" StrokeThickness="2" Stroke="{StaticResource SecondaryAccentBrush}" />
</Canvas>
<ContentControl x:Name="PropertyTimeLine" s:View.Model="{Binding PropertyTimeline}" />
@ -102,11 +136,10 @@
<Slider Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="10"
TickFrequency="5"
Minimum="1"
Maximum="600"
Minimum="31"
Maximum="350"
Value="{Binding PixelsPerSecond}"
Width="600" />
Width="319" />
</StackPanel>
</StackPanel>
</Grid>

View File

@ -1,4 +1,5 @@
using System.Windows.Controls;
using System.Windows.Input;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{

View File

@ -2,28 +2,39 @@
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.Core.Events;
using Artemis.Core.Models.Profile;
using Artemis.Core.Services;
using Artemis.Core.Services.Interfaces;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
public class LayerPropertiesViewModel : ProfileEditorPanelViewModel
{
private readonly ICoreService _coreService;
private readonly ILayerPropertyViewModelFactory _layerPropertyViewModelFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
public LayerPropertiesViewModel(IProfileEditorService profileEditorService,
ICoreService coreService,
ISettingsService settingsService,
ILayerPropertyViewModelFactory layerPropertyViewModelFactory,
IPropertyTreeViewModelFactory propertyTreeViewModelFactory,
IPropertyTimelineViewModelFactory propertyTimelineViewModelFactory)
{
_profileEditorService = profileEditorService;
_coreService = coreService;
_settingsService = settingsService;
_layerPropertyViewModelFactory = layerPropertyViewModelFactory;
PixelsPerSecond = 1;
PixelsPerSecond = 31;
PropertyTree = propertyTreeViewModelFactory.Create(this);
PropertyTimeline = propertyTimelineViewModelFactory.Create(this);
@ -33,17 +44,9 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
_profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
}
public string FormattedCurrentTime
{
get
{
if (PixelsPerSecond > 200)
return $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}";
if (PixelsPerSecond > 60)
return $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}";
return $"{Math.Floor(_profileEditorService.CurrentTime.TotalMinutes):0}:{_profileEditorService.CurrentTime.Seconds:00}";
}
}
public bool Playing { get; set; }
public string FormattedCurrentTime => $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}";
public int PixelsPerSecond
{
@ -90,48 +93,145 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
NotifyOfPropertyChange(() => TimeCaretPosition);
}
protected override void OnDeactivate()
{
Pause();
base.OnDeactivate();
}
#region Controls
public void PlayFromStart()
{
if (!IsActive)
return;
if (Playing)
Pause();
_profileEditorService.CurrentTime = TimeSpan.Zero;
Play();
}
public void Play()
{
if (!IsActive)
return;
if (Playing)
{
Pause();
return;
}
_coreService.FrameRendering += CoreServiceOnFrameRendering;
Playing = true;
}
public void Pause()
{
if (!Playing)
return;
_coreService.FrameRendering -= CoreServiceOnFrameRendering;
Playing = false;
}
public void GoToStart()
{
_profileEditorService.CurrentTime = TimeSpan.Zero;
}
public void GoToEnd()
{
_profileEditorService.CurrentTime = CalculateEndTime();
}
public void GoToPreviousFrame()
{
var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value;
var newTime = Math.Max(0, Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
_profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
public void GoToNextFrame()
{
var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value;
var newTime = Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
newTime = Math.Min(newTime, CalculateEndTime().TotalMilliseconds);
_profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
private TimeSpan CalculateEndTime()
{
// End time is the last keyframe + 10 sec
var lastKeyFrame = PropertyTimeline.PropertyTrackViewModels.SelectMany(r => r.KeyframeViewModels).OrderByDescending(t => t.Keyframe.Position).FirstOrDefault();
return lastKeyFrame?.Keyframe.Position.Add(new TimeSpan(0, 0, 0, 10)) ?? TimeSpan.FromSeconds(10);
}
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
{
Execute.PostToUIThread(() =>
{
var newTime = _profileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
if (newTime > CalculateEndTime())
{
newTime = CalculateEndTime();
Pause();
}
_profileEditorService.CurrentTime = newTime;
});
}
#endregion
#region Caret movement
private double _caretStartMouseStartOffset;
private bool _mouseOverCaret;
private int _pixelsPerSecond;
public void TimelineMouseDown(object sender, MouseButtonEventArgs e)
{
// TODO Preserve mouse offset
_caretStartMouseStartOffset = e.GetPosition((IInputElement) sender).X - TimeCaretPosition.Left;
((IInputElement) sender).CaptureMouse();
}
public void CaretMouseEnter(object sender, MouseEventArgs e)
public void TimelineMouseUp(object sender, MouseButtonEventArgs e)
{
_mouseOverCaret = true;
}
public void CaretMouseLeave(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
_mouseOverCaret = false;
((IInputElement) sender).ReleaseMouseCapture();
}
public void TimelineMouseMove(object sender, MouseEventArgs e)
{
if (_mouseOverCaret && e.LeftButton == MouseButtonState.Pressed)
if (e.LeftButton == MouseButtonState.Pressed)
{
// Snap to visible keyframes
var visibleKeyframes = PropertyTimeline.PropertyTrackViewModels.Where(t => t.LayerPropertyViewModel.Parent != null &&
t.LayerPropertyViewModel.Parent.IsExpanded)
// Get the parent grid, need that for our position
var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
var x = Math.Max(0, e.GetPosition(parent).X);
var newTime = TimeSpan.FromSeconds(x / PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (PixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
{
_profileEditorService.CurrentTime = newTime;
return;
}
// If shift is held, snap to closest keyframe
var visibleKeyframes = PropertyTimeline.PropertyTrackViewModels
.Where(t => t.LayerPropertyViewModel.Parent != null && t.LayerPropertyViewModel.Parent.IsExpanded)
.SelectMany(t => t.KeyframeViewModels);
TimeCaretPosition = new Thickness(Math.Max(0, e.GetPosition((IInputElement) sender).X + _caretStartMouseStartOffset), 0, 0, 0);
// Take a tolerance of 5 pixels (half a keyframe width)
var tolerance = 1000f / PixelsPerSecond * 5;
var closeKeyframe = visibleKeyframes.FirstOrDefault(
kf => Math.Abs(kf.Keyframe.Position.TotalMilliseconds - _profileEditorService.CurrentTime.TotalMilliseconds) < tolerance
kf => Math.Abs(kf.Keyframe.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance
);
if (closeKeyframe != null)
_profileEditorService.CurrentTime = closeKeyframe.Keyframe.Position;
_profileEditorService.CurrentTime = closeKeyframe?.Keyframe.Position ?? newTime;
}
}

View File

@ -24,6 +24,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
LayerProperty = layerProperty;
Parent = parent;
Children = new List<LayerPropertyViewModel>();
IsExpanded = layerProperty.ExpandByDefault;
foreach (var child in layerProperty.Children)
Children.Add(layerPropertyViewModelFactory.Create(child, this));

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Services.Interfaces;
using Stylet;
@ -9,10 +10,14 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
public class PropertyTimelineViewModel : PropertyChangedBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly IPropertyTrackViewModelFactory _propertyTrackViewModelFactory;
public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IProfileEditorService profileEditorService)
public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel,
IProfileEditorService profileEditorService,
IPropertyTrackViewModelFactory propertyTrackViewModelFactory)
{
_profileEditorService = profileEditorService;
_propertyTrackViewModelFactory = propertyTrackViewModelFactory;
LayerPropertiesViewModel = layerPropertiesViewModel;
PropertyTrackViewModels = new BindableCollection<PropertyTrackViewModel>();
@ -50,7 +55,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
private void CreateViewModels(LayerPropertyViewModel property)
{
PropertyTrackViewModels.Add(new PropertyTrackViewModel(this, property));
PropertyTrackViewModels.Add(_propertyTrackViewModelFactory.Create(this, property));
foreach (var child in property.Children)
CreateViewModels(child);
}

View File

@ -0,0 +1,51 @@
using System.Windows;
using System.Windows.Media;
using Artemis.Core.Utilities;
using Humanizer;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTrackEasingViewModel : PropertyChangedBase
{
private readonly PropertyTrackKeyframeViewModel _keyframeViewModel;
private bool _isEasingModeSelected;
public PropertyTrackEasingViewModel(PropertyTrackKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
{
_keyframeViewModel = keyframeViewModel;
_isEasingModeSelected = keyframeViewModel.Keyframe.EasingFunction == easingFunction;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
CreateGeometry();
}
public Easings.Functions EasingFunction { get; }
public PointCollection EasingPoints { get; set; }
public string Description { get; set; }
public bool IsEasingModeSelected
{
get => _isEasingModeSelected;
set
{
_isEasingModeSelected = value;
if (_isEasingModeSelected)
_keyframeViewModel.SelectEasingMode(this);
}
}
private void CreateGeometry()
{
EasingPoints = new PointCollection();
for (var i = 1; i <= 10; i++)
{
var x = i;
var y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
EasingPoints.Add(new Point(x, y));
}
}
}
}

View File

@ -1,25 +1,114 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Utilities;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTrackKeyframeViewModel : PropertyChangedBase
{
public PropertyTrackKeyframeViewModel(BaseKeyframe keyframe)
private readonly IProfileEditorService _profileEditorService;
private int _pixelsPerSecond;
public PropertyTrackKeyframeViewModel(BaseKeyframe keyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
Keyframe = keyframe;
EasingViewModels = new BindableCollection<PropertyTrackEasingViewModel>();
CreateEasingViewModels();
}
public BaseKeyframe Keyframe { get; }
public BindableCollection<PropertyTrackEasingViewModel> EasingViewModels { get; set; }
public double X { get; set; }
public string Timestamp { get; set; }
public UIElement ParentView { get; set; }
public void Update(int pixelsPerSecond)
{
_pixelsPerSecond = pixelsPerSecond;
X = pixelsPerSecond * Keyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(Keyframe.Position.TotalSeconds):00}.{Keyframe.Position.Milliseconds:000}";
}
#region Keyframe movement
public void KeyframeMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
}
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
}
public void KeyframeMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
// Get the parent grid, need that for our position
var x = Math.Max(0, e.GetPosition(ParentView).X);
var newTime = TimeSpan.FromSeconds(x / _pixelsPerSecond);
// Round the time to something that fits the current zoom level
if (_pixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (_pixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
{
Keyframe.Position = newTime;
Update(_pixelsPerSecond);
_profileEditorService.UpdateSelectedProfileElement();
return;
}
// If shift is held, snap to the current time
// Take a tolerance of 5 pixels (half a keyframe width)
var tolerance = 1000f / _pixelsPerSecond * 5;
if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance)
Keyframe.Position = _profileEditorService.CurrentTime;
else
Keyframe.Position = newTime;
Update(_pixelsPerSecond);
_profileEditorService.UpdateSelectedProfileElement();
}
}
#endregion
#region Easing
private void CreateEasingViewModels()
{
foreach (Easings.Functions value in Enum.GetValues(typeof(Easings.Functions)))
EasingViewModels.Add(new PropertyTrackEasingViewModel(this, value));
}
public void SelectEasingMode(PropertyTrackEasingViewModel easingViewModel)
{
Keyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
_profileEditorService.UpdateSelectedProfileElement();
}
#endregion
}
}

View File

@ -28,7 +28,48 @@
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}" Width="10" Height="10" Margin="-5,6,0,0" ToolTip="{Binding Timestamp}" />
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}"
Width="10"
Height="10"
Margin="-5,6,0,0"
ToolTip="{Binding Timestamp}"
s:View.ActionTarget="{Binding}"
MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}">
<Ellipse.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy" />
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked" Value="{Binding Path=IsEasingModeSelected, Mode=TwoWay}" />
</Style>
</MenuItem.ItemContainerStyle>
<MenuItem.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<!-- <MenuItem Header="Easing mode" IsEnabled="{Binding CanSelectEasingMode}"> -->
<!-- <MenuItem Header="Ease in" Command="{s:Action SetEasingMode}" CommandParameter="EaseIn" /> -->
<!-- <MenuItem Header="Ease out" Command="{s:Action SetEasingMode}" CommandParameter="EaseOut" /> -->
<!-- <MenuItem Header="Ease in and out" Command="{s:Action SetEasingMode}" CommandParameter="EaseInOut" /> -->
<!-- </MenuItem> -->
</ContextMenu>
</Ellipse.ContextMenu>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

View File

@ -1,12 +1,18 @@
using System.Linq;
using Artemis.UI.Ninject.Factories;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTrackViewModel : PropertyChangedBase
public class PropertyTrackViewModel : Screen
{
public PropertyTrackViewModel(PropertyTimelineViewModel propertyTimelineViewModel, LayerPropertyViewModel layerPropertyViewModel)
private readonly IPropertyTrackKeyframeViewModelFactory _propertyTrackKeyframeViewModelFactory;
public PropertyTrackViewModel(PropertyTimelineViewModel propertyTimelineViewModel,
LayerPropertyViewModel layerPropertyViewModel,
IPropertyTrackKeyframeViewModelFactory propertyTrackKeyframeViewModelFactory)
{
_propertyTrackKeyframeViewModelFactory = propertyTrackKeyframeViewModelFactory;
PropertyTimelineViewModel = propertyTimelineViewModel;
LayerPropertyViewModel = layerPropertyViewModel;
KeyframeViewModels = new BindableCollection<PropertyTrackKeyframeViewModel>();
@ -33,7 +39,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
if (KeyframeViewModels.Any(k => k.Keyframe == keyframe))
continue;
KeyframeViewModels.Add(new PropertyTrackKeyframeViewModel(keyframe));
KeyframeViewModels.Add(_propertyTrackKeyframeViewModelFactory.Create(keyframe));
}
UpdateKeyframes(PropertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond);
@ -42,7 +48,17 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
public void UpdateKeyframes(int pixelsPerSecond)
{
foreach (var keyframeViewModel in KeyframeViewModels)
{
keyframeViewModel.ParentView = View;
keyframeViewModel.Update(pixelsPerSecond);
}
}
protected override void OnViewLoaded()
{
foreach (var keyframeViewModel in KeyframeViewModels)
keyframeViewModel.ParentView = View;
base.OnViewLoaded();
}
}
}

View File

@ -8,8 +8,9 @@
xmlns:visualization="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileDeviceViewModel}}"
d:DesignHeight="450" d:DesignWidth="800">
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileDeviceViewModel}}">
<UserControl.Resources>
<converters:NullToImageConverter x:Key="NullToImageConverter" />
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />

View File

@ -5,7 +5,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type local:ProfileLayerViewModel}}">
<UserControl.Resources>
<Style TargetType="{x:Type Canvas}" x:Key="SelectedStyle">
<Style.Triggers>
@ -61,5 +63,13 @@
<SolidColorBrush Color="{StaticResource Accent400}" />
</Path.Stroke>
</Path>
<!-- The rectangle around the shape that allows modification -->
<Rectangle Width="{Binding ShapeRectangle.Width}"
Height="{Binding ShapeRectangle.Height}"
Canvas.Left="{Binding ShapeRectangle.X}"
Canvas.Top="{Binding ShapeRectangle.Y}" Stroke="{DynamicResource PrimaryHueMidBrush}"
StrokeThickness="1"
StrokeDashArray="2 2" />
</Canvas>
</UserControl>

View File

@ -33,6 +33,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
public Geometry LayerGeometry { get; set; }
public Geometry OpacityGeometry { get; set; }
public Geometry ShapeGeometry { get; set; }
public Rect ShapeRectangle { get; set; }
public Rect ViewportRectangle { get; set; }
public bool IsSelected { get; set; }
@ -99,22 +100,22 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
}
var skRect = Layer.LayerShape.GetUnscaledRectangle();
var rect = new Rect(skRect.Left, skRect.Top, skRect.Width, skRect.Height);
ShapeRectangle = new Rect(skRect.Left, skRect.Top, skRect.Width, skRect.Height);
var shapeGeometry = Geometry.Empty;
switch (Layer.LayerShape)
{
case Ellipse _:
shapeGeometry = new EllipseGeometry(rect);
shapeGeometry = new EllipseGeometry(ShapeRectangle);
break;
case Fill _:
shapeGeometry = LayerGeometry;
break;
case Polygon _:
// TODO
shapeGeometry = new RectangleGeometry(rect);
shapeGeometry = new RectangleGeometry(ShapeRectangle);
break;
case Rectangle _:
shapeGeometry = new RectangleGeometry(rect);
shapeGeometry = new RectangleGeometry(ShapeRectangle);
break;
}

View File

@ -114,6 +114,33 @@
</VisualBrush>
</Grid.Background>
<Grid Name="DeviceDisplayGrid">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="{Binding PanZoomViewModel.Zoom}" ScaleY="{Binding PanZoomViewModel.Zoom}" />
<TranslateTransform X="{Binding PanZoomViewModel.PanX}" Y="{Binding PanZoomViewModel.PanY}" />
</TransformGroup>
</Grid.RenderTransform>
<ItemsControl ItemsSource="{Binding DeviceViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Grid Name="EditorDisplayGrid">
<Grid.RenderTransform>
<TransformGroup>
@ -141,6 +168,8 @@
</ItemsControl>
</Grid>
<StackPanel ZIndex="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="10">
<materialDesign:Card Padding="8">
<StackPanel Orientation="Horizontal">

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using Artemis.Core.Events;
using Artemis.Core.Models.Profile;
@ -45,6 +46,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
Execute.OnUIThreadSync(() =>
{
CanvasViewModels = new ObservableCollection<CanvasViewModel>();
DeviceViewModels = new ObservableCollection<ProfileDeviceViewModel>();
PanZoomViewModel = new PanZoomViewModel();
});
@ -62,16 +64,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
public bool IsInitializing { get; private set; }
public ObservableCollection<CanvasViewModel> CanvasViewModels { get; set; }
public ObservableCollection<ProfileDeviceViewModel> DeviceViewModels { get; set; }
public PanZoomViewModel PanZoomViewModel { get; set; }
public PluginSetting<bool> HighlightSelectedLayer { get; set; }
public PluginSetting<bool> PauseRenderingOnFocusLoss { get; set; }
public ReadOnlyCollection<ProfileDeviceViewModel> Devices => CanvasViewModels
.Where(vm => vm is ProfileDeviceViewModel)
.Cast<ProfileDeviceViewModel>()
.ToList()
.AsReadOnly();
public VisualizationToolViewModel ActiveToolViewModel
{
get => _activeToolViewModel;
@ -141,7 +138,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
var layerViewModels = CanvasViewModels.Where(vm => vm is ProfileLayerViewModel).Cast<ProfileLayerViewModel>().ToList();
var layers = _profileEditorService.SelectedProfile?.GetAllLayers() ?? new List<Layer>();
// Add new layers missing a VM
foreach (var layer in layers)
{
@ -156,74 +152,51 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
profileLayerViewModel.Dispose();
CanvasViewModels.Remove(profileLayerViewModel);
}
// Sort the devices by ZIndex
Execute.PostToUIThread(() =>
{
foreach (var device in Devices.ToList())
CanvasViewModels.Move(CanvasViewModels.IndexOf(device), device.Device.ZIndex - 1);
});
}
});
}
private void ApplySurfaceConfiguration(ArtemisSurface surface)
{
var devices = new List<ArtemisDevice>();
devices.AddRange(surface.Devices);
// Make sure all devices have an up-to-date VM
foreach (var surfaceDeviceConfiguration in devices)
{
// Create VMs for missing devices
ProfileDeviceViewModel viewModel;
lock (CanvasViewModels)
{
viewModel = Devices.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice);
}
if (viewModel == null)
{
// Create outside the UI thread to avoid slowdowns as much as possible
var profileDeviceViewModel = new ProfileDeviceViewModel(surfaceDeviceConfiguration);
Execute.PostToUIThread(() =>
{
// Gotta call IsInitializing on the UI thread or its never gets picked up
IsInitializing = true;
lock (CanvasViewModels)
{
CanvasViewModels.Add(profileDeviceViewModel);
}
});
}
// Update existing devices
else
viewModel.Device = surfaceDeviceConfiguration;
}
// Sort the devices by ZIndex
Execute.PostToUIThread(() =>
{
lock (CanvasViewModels)
lock (DeviceViewModels)
{
foreach (var device in Devices.OrderBy(d => d.ZIndex).ToList())
var existing = DeviceViewModels.ToList();
var deviceViewModels = new List<ProfileDeviceViewModel>();
// Add missing/update existing
foreach (var surfaceDeviceConfiguration in surface.Devices.OrderBy(d => d.ZIndex).ToList())
{
var newIndex = Math.Max(device.ZIndex - 1, CanvasViewModels.Count - 1);
CanvasViewModels.Move(CanvasViewModels.IndexOf(device), newIndex);
// Create VMs for missing devices
var viewModel = existing.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice);
if (viewModel == null)
{
IsInitializing = true;
viewModel = new ProfileDeviceViewModel(surfaceDeviceConfiguration);
}
// Update existing devices
else
viewModel.Device = surfaceDeviceConfiguration;
// Add the viewModel to the list of VMs we want to keep
deviceViewModels.Add(viewModel);
}
DeviceViewModels = new ObservableCollection<ProfileDeviceViewModel>(deviceViewModels);
}
});
}
private void UpdateLeds(object sender, CustomUpdateData customUpdateData)
{
lock (CanvasViewModels)
lock (DeviceViewModels)
{
if (IsInitializing)
IsInitializing = Devices.Any(d => !d.AddedLeds);
IsInitializing = DeviceViewModels.Any(d => !d.AddedLeds);
foreach (var profileDeviceViewModel in Devices)
foreach (var profileDeviceViewModel in DeviceViewModels)
profileDeviceViewModel.Update();
}
}
@ -232,12 +205,12 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
{
if (HighlightSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer)
{
foreach (var led in Devices.SelectMany(d => d.Leds))
foreach (var led in DeviceViewModels.SelectMany(d => d.Leds))
led.IsDimmed = !layer.Leds.Contains(led.Led);
}
else
{
foreach (var led in Devices.SelectMany(d => d.Leds))
foreach (var led in DeviceViewModels.SelectMany(d => d.Leds))
led.IsDimmed = false;
}
}
@ -316,11 +289,13 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
public void CanvasMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
ActiveToolViewModel?.MouseDown(sender, e);
}
public void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
ActiveToolViewModel?.MouseUp(sender, e);
}
@ -351,26 +326,26 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
return;
layer.ClearLeds();
layer.AddLeds(Devices.SelectMany(d => d.Leds).Where(vm => vm.IsSelected).Select(vm => vm.Led));
layer.AddLeds(DeviceViewModels.SelectMany(d => d.Leds).Where(vm => vm.IsSelected).Select(vm => vm.Led));
_profileEditorService.UpdateSelectedProfileElement();
}
public void SelectAll()
{
foreach (var ledVm in Devices.SelectMany(d => d.Leds))
foreach (var ledVm in DeviceViewModels.SelectMany(d => d.Leds))
ledVm.IsSelected = true;
}
public void InverseSelection()
{
foreach (var ledVm in Devices.SelectMany(d => d.Leds))
foreach (var ledVm in DeviceViewModels.SelectMany(d => d.Leds))
ledVm.IsSelected = !ledVm.IsSelected;
}
public void ClearSelection()
{
foreach (var ledVm in Devices.SelectMany(d => d.Leds))
foreach (var ledVm in DeviceViewModels.SelectMany(d => d.Leds))
ledVm.IsSelected = false;
}
@ -421,7 +396,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
public void Handle(MainWindowKeyEvent message)
{
Debug.WriteLine(message.KeyDown);
if (message.KeyDown)
{
if (ActiveToolIndex != 0)

View File

@ -23,7 +23,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(MouseDownStartPosition, position);
foreach (var device in ProfileViewModel.Devices)
foreach (var device in ProfileViewModel.DeviceViewModels)
{
foreach (var ledViewModel in device.Leds)
{

View File

@ -23,7 +23,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(MouseDownStartPosition, position);
foreach (var device in ProfileViewModel.Devices)
foreach (var device in ProfileViewModel.DeviceViewModels)
{
foreach (var ledViewModel in device.Leds)
{

View File

@ -33,7 +33,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools
// Get selected LEDs
var selectedLeds = new List<ArtemisLed>();
foreach (var device in ProfileViewModel.Devices)
foreach (var device in ProfileViewModel.DeviceViewModels)
{
foreach (var ledViewModel in device.Leds)
{
@ -90,7 +90,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools
var position = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e);
var selectedRect = new Rect(MouseDownStartPosition, position);
foreach (var device in ProfileViewModel.Devices)
foreach (var device in ProfileViewModel.DeviceViewModels)
{
foreach (var ledViewModel in device.Leds)
{

View File

@ -64,7 +64,7 @@ namespace Artemis.UI.Screens.SurfaceEditor
ApplySelectedSurfaceConfiguration();
}
}
public ArtemisSurface CreateSurfaceConfiguration(string name)
{
var config = _surfaceService.CreateSurfaceConfiguration(name);
@ -111,22 +111,32 @@ namespace Artemis.UI.Screens.SurfaceEditor
private void ApplySelectedSurfaceConfiguration()
{
// Make sure all devices have an up-to-date VM
foreach (var surfaceDeviceConfiguration in SelectedSurface.Devices)
{
// Create VMs for missing devices
var viewModel = Devices.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice);
if (viewModel == null)
Execute.PostToUIThread(() => Devices.Add(new SurfaceDeviceViewModel(surfaceDeviceConfiguration)));
// Update existing devices
else
viewModel.Device = surfaceDeviceConfiguration;
}
// Sort the devices by ZIndex
Execute.PostToUIThread(() =>
{
foreach (var device in Devices.OrderBy(d => d.Device.ZIndex).ToList())
Devices.Move(Devices.IndexOf(device), device.Device.ZIndex - 1);
lock (Devices)
{
var existing = Devices.ToList();
var deviceViewModels = new List<SurfaceDeviceViewModel>();
// Add missing/update existing
foreach (var surfaceDeviceConfiguration in SelectedSurface.Devices.OrderBy(d => d.ZIndex).ToList())
{
// Create VMs for missing devices
var viewModel = existing.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice);
if (viewModel == null)
{
viewModel = new SurfaceDeviceViewModel(surfaceDeviceConfiguration);
}
// Update existing devices
else
viewModel.Device = surfaceDeviceConfiguration;
// Add the viewModel to the list of VMs we want to keep
deviceViewModels.Add(viewModel);
}
Devices = new ObservableCollection<SurfaceDeviceViewModel>(deviceViewModels);
}
});
_surfaceService.SetActiveSurfaceConfiguration(SelectedSurface);
@ -189,6 +199,7 @@ namespace Artemis.UI.Screens.SurfaceEditor
var deviceViewModel = Devices[i];
deviceViewModel.Device.ZIndex = i + 1;
}
_surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true);
}
public void BringForward(SurfaceDeviceViewModel surfaceDeviceViewModel)
@ -202,6 +213,7 @@ namespace Artemis.UI.Screens.SurfaceEditor
var deviceViewModel = Devices[i];
deviceViewModel.Device.ZIndex = i + 1;
}
_surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true);
}
public void SendToBack(SurfaceDeviceViewModel surfaceDeviceViewModel)
@ -212,6 +224,7 @@ namespace Artemis.UI.Screens.SurfaceEditor
var deviceViewModel = Devices[i];
deviceViewModel.Device.ZIndex = i + 1;
}
_surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true);
}
public void SendBackward(SurfaceDeviceViewModel surfaceDeviceViewModel)
@ -224,6 +237,7 @@ namespace Artemis.UI.Screens.SurfaceEditor
var deviceViewModel = Devices[i];
deviceViewModel.Device.ZIndex = i + 1;
}
_surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true);
}
public async Task ViewProperties(SurfaceDeviceViewModel surfaceDeviceViewModel)
@ -248,6 +262,11 @@ namespace Artemis.UI.Screens.SurfaceEditor
// ReSharper disable once UnusedMember.Global - Called from view
public void EditorGridMouseClick(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
((IInputElement) sender).CaptureMouse();
else
((IInputElement) sender).ReleaseMouseCapture();
if (IsPanKeyDown() || e.ChangedButton == MouseButton.Right)
return;

View File

@ -35,12 +35,14 @@ namespace Artemis.UI.Services
public void ChangeSelectedProfile(Profile profile)
{
SelectedProfile = profile;
UpdateProfilePreview();
OnSelectedProfileChanged();
}
public void UpdateSelectedProfile()
{
_profileService.UpdateProfile(SelectedProfile, false);
UpdateProfilePreview();
OnSelectedProfileElementUpdated();
}
@ -53,12 +55,15 @@ namespace Artemis.UI.Services
public void UpdateSelectedProfileElement()
{
_profileService.UpdateProfile(SelectedProfile, true);
UpdateProfilePreview();
OnSelectedProfileElementUpdated();
}
private void UpdateProfilePreview()
{
if (SelectedProfile == null)
return;
var delta = CurrentTime - _lastUpdateTime;
foreach (var layer in SelectedProfile.GetAllLayers())
{