diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 728848333..1cc4688fa 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerEffect.Abstract; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; @@ -24,6 +25,7 @@ namespace Artemis.Core.Models.Profile _layerEffects = new List(); _expandedPropertyGroups = new List(); + ApplyRenderElementDefaults(); } internal Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) @@ -42,8 +44,6 @@ namespace Artemis.Core.Models.Profile _expandedPropertyGroups = new List(); _expandedPropertyGroups.AddRange(folderEntity.ExpandedPropertyGroups); - // TODO: Load conditions - // Load child folders foreach (var childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId)) ChildrenList.Add(new Folder(profile, this, childFolder)); @@ -55,6 +55,8 @@ namespace Artemis.Core.Models.Profile ChildrenList = ChildrenList.OrderBy(c => c.Order).ToList(); for (var index = 0; index < ChildrenList.Count; index++) ChildrenList[index].Order = index + 1; + + ApplyRenderElementEntity(); } internal FolderEntity FolderEntity { get; set; } @@ -187,7 +189,7 @@ namespace Artemis.Core.Models.Profile FolderEntity.ExpandedPropertyGroups.Clear(); FolderEntity.ExpandedPropertyGroups.AddRange(_expandedPropertyGroups); - ApplyLayerEffectsToEntity(); + ApplyRenderElementToEntity(); // Conditions RenderElementEntity.RootDisplayCondition = DisplayConditionGroup?.DisplayConditionGroupEntity; diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 2aa064252..7bdc53f35 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Extensions; -using Artemis.Core.Models.Profile.Conditions; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Models.Profile.LayerProperties.Attributes; using Artemis.Core.Models.Profile.LayerShapes; @@ -48,6 +47,7 @@ namespace Artemis.Core.Models.Profile _expandedPropertyGroups = new List(); General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized; + ApplyRenderElementDefaults(); } internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) @@ -69,6 +69,8 @@ namespace Artemis.Core.Models.Profile _expandedPropertyGroups.AddRange(layerEntity.ExpandedPropertyGroups); General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized; + ApplyRenderElementEntity(); + ApplyRenderElementDefaults(); } internal LayerEntity LayerEntity { get; set; } @@ -115,14 +117,27 @@ namespace Artemis.Core.Models.Profile get => _layerBrush; internal set => SetAndNotify(ref _layerBrush, value); } - - - + public override string ToString() { return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } + /// + public override List GetAllKeyframes() + { + var keyframes = base.GetAllKeyframes(); + + foreach (var baseLayerProperty in General.GetAllLayerProperties()) + keyframes.AddRange(baseLayerProperty.BaseKeyframes); + foreach (var baseLayerProperty in Transform.GetAllLayerProperties()) + keyframes.AddRange(baseLayerProperty.BaseKeyframes); + foreach (var baseLayerProperty in LayerBrush.BaseProperties.GetAllLayerProperties()) + keyframes.AddRange(baseLayerProperty.BaseKeyframes); + + return keyframes; + } + #region Storage internal override void ApplyToEntity() @@ -142,7 +157,7 @@ namespace Artemis.Core.Models.Profile LayerBrush?.BaseProperties.ApplyToEntity(); // Effects - ApplyLayerEffectsToEntity(); + ApplyRenderElementToEntity(); // LEDs LayerEntity.Leds.Clear(); @@ -206,34 +221,18 @@ namespace Artemis.Core.Models.Profile return; UpdateDisplayCondition(); + deltaTime = UpdateTimeline(deltaTime); - // TODO: Remove, this is slow and stupid - // For now, reset all keyframe engines after the last keyframe was hit - // This is a placeholder method of repeating the animation until repeat modes are implemented - var properties = new List(General.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any())); - properties.AddRange(Transform.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any())); - properties.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any())); - var timeLineEnd = properties.Any() ? properties.Max(p => p.BaseKeyframes.Max(k => k.Position)) : TimeSpan.MaxValue; - if (properties.Any(p => p.TimelineProgress >= timeLineEnd)) - { - General.Override(TimeSpan.Zero); - Transform.Override(TimeSpan.Zero); - LayerBrush.BaseProperties.Override(TimeSpan.Zero); - foreach (var baseLayerEffect in LayerEffects.Where(e => e.Enabled)) - baseLayerEffect.BaseProperties?.Override(TimeSpan.Zero); - } - else - { - General.Update(deltaTime); - Transform.Update(deltaTime); - LayerBrush.BaseProperties.Update(deltaTime); - foreach (var baseLayerEffect in LayerEffects.Where(e => e.Enabled)) - baseLayerEffect.BaseProperties?.Update(deltaTime); - } - + General.Update(deltaTime); + Transform.Update(deltaTime); + LayerBrush.BaseProperties?.Update(deltaTime); LayerBrush.Update(deltaTime); + foreach (var baseLayerEffect in LayerEffects.Where(e => e.Enabled)) + { + baseLayerEffect.BaseProperties?.Update(deltaTime); baseLayerEffect.Update(deltaTime); + } } public void OverrideProgress(TimeSpan timeOverride) diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index bbc5206ad..b5e34b35e 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -5,6 +5,7 @@ using System.Linq; using Artemis.Core.Annotations; using Artemis.Core.Models.Profile.Conditions; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Plugins.LayerBrush.Abstract; using Artemis.Core.Plugins.LayerEffect.Abstract; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; @@ -14,6 +15,62 @@ namespace Artemis.Core.Models.Profile { public abstract class RenderProfileElement : ProfileElement { + protected void ApplyRenderElementDefaults() + { + if (MainSegmentLength <= TimeSpan.Zero) + MainSegmentLength = TimeSpan.FromSeconds(5); + } + + protected void ApplyRenderElementEntity() + { + StartSegmentLength = RenderElementEntity.StartSegmentLength; + MainSegmentLength = RenderElementEntity.MainSegmentLength; + EndSegmentLength = RenderElementEntity.EndSegmentLength; + RepeatMainSegment = RenderElementEntity.RepeatMainSegment; + AlwaysFinishTimeline = RenderElementEntity.AlwaysFinishTimeline; + } + + protected void ApplyRenderElementToEntity() + { + RenderElementEntity.StartSegmentLength = StartSegmentLength; + RenderElementEntity.MainSegmentLength = MainSegmentLength; + RenderElementEntity.EndSegmentLength = EndSegmentLength; + RenderElementEntity.RepeatMainSegment = RepeatMainSegment; + RenderElementEntity.AlwaysFinishTimeline = AlwaysFinishTimeline; + + RenderElementEntity.LayerEffects.Clear(); + foreach (var layerEffect in LayerEffects) + { + var layerEffectEntity = new LayerEffectEntity + { + Id = layerEffect.EntityId, + PluginGuid = layerEffect.PluginInfo.Guid, + EffectType = layerEffect.GetType().Name, + Name = layerEffect.Name, + Enabled = layerEffect.Enabled, + HasBeenRenamed = layerEffect.HasBeenRenamed, + Order = layerEffect.Order + }; + RenderElementEntity.LayerEffects.Add(layerEffectEntity); + layerEffect.BaseProperties.ApplyToEntity(); + } + } + + /// + /// Returns a list of all keyframes on all properties and effects of this layer + /// + public virtual List GetAllKeyframes() + { + var keyframes = new List(); + foreach (var layerEffect in LayerEffects) + { + foreach (var baseLayerProperty in layerEffect.BaseProperties.GetAllLayerProperties()) + keyframes.AddRange(baseLayerProperty.BaseKeyframes); + } + + return keyframes; + } + #region Properties private SKPath _path; @@ -101,6 +158,15 @@ namespace Artemis.Core.Models.Profile set => SetAndNotify(ref _endSegmentLength, value); } + /// + /// Gets the current timeline position + /// + public TimeSpan TimelinePosition + { + get => _timelinePosition; + private set => SetAndNotify(ref _timelinePosition, value); + } + /// /// Gets the total combined length of all three segments /// @@ -124,6 +190,35 @@ namespace Artemis.Core.Models.Profile set => SetAndNotify(ref _alwaysFinishTimeline, value); } + protected double UpdateTimeline(double deltaTime) + { + var oldPosition = _timelinePosition; + var deltaTimeSpan = TimeSpan.FromSeconds(deltaTime); + var mainSegmentEnd = StartSegmentLength + MainSegmentLength; + + TimelinePosition += deltaTimeSpan; + + // Manage segments while the condition is met + if (DisplayConditionMet) + { + // If we are at the end of the main timeline, wrap around back to the beginning + if (RepeatMainSegment && TimelinePosition >= mainSegmentEnd) + TimelinePosition = StartSegmentLength + (mainSegmentEnd - TimelinePosition); + else if (TimelinePosition >= TimelineLength) + TimelinePosition = TimelineLength; + } + else + { + // Skip to the last segment if conditions are no longer met + if (!AlwaysFinishTimeline && TimelinePosition < mainSegmentEnd) + TimelinePosition = mainSegmentEnd; + else if (TimelinePosition >= TimelineLength) + TimelinePosition = TimelineLength; + } + + return (TimelinePosition - oldPosition).TotalSeconds; + } + #endregion #region Effects @@ -135,26 +230,6 @@ namespace Artemis.Core.Models.Profile /// public ReadOnlyCollection LayerEffects => _layerEffects.AsReadOnly(); - protected void ApplyLayerEffectsToEntity() - { - RenderElementEntity.LayerEffects.Clear(); - foreach (var layerEffect in LayerEffects) - { - var layerEffectEntity = new LayerEffectEntity - { - Id = layerEffect.EntityId, - PluginGuid = layerEffect.PluginInfo.Guid, - EffectType = layerEffect.GetType().Name, - Name = layerEffect.Name, - Enabled = layerEffect.Enabled, - HasBeenRenamed = layerEffect.HasBeenRenamed, - Order = layerEffect.Order - }; - RenderElementEntity.LayerEffects.Add(layerEffectEntity); - layerEffect.BaseProperties.ApplyToEntity(); - } - } - internal void RemoveLayerEffect([NotNull] BaseLayerEffect effect) { if (effect == null) throw new ArgumentNullException(nameof(effect)); @@ -192,8 +267,20 @@ namespace Artemis.Core.Models.Profile #region Conditions + /// + /// Gets whether the display conditions applied to this layer where met or not during last update + /// Always true if the layer has no display conditions + /// + public bool DisplayConditionMet + { + get => _displayConditionMet; + private set => SetAndNotify(ref _displayConditionMet, value); + } + private DisplayConditionGroup _displayConditionGroup; - + private TimeSpan _timelinePosition; + private bool _displayConditionMet; + /// /// Gets or sets the root display condition group /// @@ -205,7 +292,11 @@ namespace Artemis.Core.Models.Profile public void UpdateDisplayCondition() { - var rootGroupResult = DisplayConditionGroup.Evaluate(); + var conditionMet = DisplayConditionGroup == null || DisplayConditionGroup.Evaluate(); + if (conditionMet && !DisplayConditionMet) + TimelinePosition = TimeSpan.Zero; + + DisplayConditionMet = conditionMet; } #endregion diff --git a/src/Artemis.Core/Plugins/LayerBrush/Abstract/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrush/Abstract/BaseLayerBrush.cs index d8277af34..fbfa4ba5a 100644 --- a/src/Artemis.Core/Plugins/LayerBrush/Abstract/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrush/Abstract/BaseLayerBrush.cs @@ -98,7 +98,7 @@ namespace Artemis.Core.Plugins.LayerBrush.Abstract /// /// Called before rendering every frame, write your update logic here /// - /// + /// Seconds passed since last update public abstract void Update(double deltaTime); /// diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index 990b545ff..b8efbc81e 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -1,9 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Artemis.Storage.Entities.Profile.Abstract { public abstract class RenderElementEntity { + public TimeSpan StartSegmentLength { get; set; } + public TimeSpan MainSegmentLength { get; set; } + public TimeSpan EndSegmentLength { get; set; } + public bool RepeatMainSegment { get; set; } + public bool AlwaysFinishTimeline { get; set; } + public List LayerEffects { get; set; } public List PropertyEntities { get; set; } public List ExpandedPropertyGroups { get; set; } diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index ec51011df..c1271a780 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -8,6 +8,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; +using System.Windows.Threading; using Artemis.Core.Models.Surface; using RGB.NET.Core; using Size = System.Windows.Size; @@ -28,17 +29,20 @@ namespace Artemis.UI.Shared.Controls private readonly DrawingGroup _backingStore; private readonly List _deviceVisualizerLeds; private BitmapImage _deviceImage; - private bool _subscribed; private ArtemisDevice _oldDevice; - private Task _lastRenderTask; + private readonly DispatcherTimer _timer; public DeviceVisualizer() { _backingStore = new DrawingGroup(); _deviceVisualizerLeds = new List(); - Loaded += (sender, args) => SubscribeToUpdate(true); - Unloaded += (sender, args) => SubscribeToUpdate(false); + // Run an update timer at 25 fps + _timer = new DispatcherTimer {Interval = TimeSpan.FromMilliseconds(40)}; + _timer.Tick += TimerOnTick; + + Loaded += (sender, args) => _timer.Start(); + Unloaded += (sender, args) => _timer.Stop(); } public ArtemisDevice Device @@ -61,7 +65,7 @@ namespace Artemis.UI.Shared.Controls public void Dispose() { - RGBSurface.Instance.Updated -= RgbSurfaceOnUpdated; + _timer.Stop(); } protected override void OnRender(DrawingContext drawingContext) @@ -73,7 +77,7 @@ namespace Artemis.UI.Shared.Controls var measureSize = MeasureOverride(Size.Empty); var scale = Math.Min(DesiredSize.Width / measureSize.Width, DesiredSize.Height / measureSize.Height); var scaledRect = new Rect(0, 0, measureSize.Width * scale, measureSize.Height * scale); - + // Center and scale the visualization in the desired bounding box if (DesiredSize.Width > 0 && DesiredSize.Height > 0) { @@ -95,7 +99,7 @@ namespace Artemis.UI.Shared.Controls // Render device and LED images if (_deviceImage != null) drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height)); - + foreach (var deviceVisualizerLed in _deviceVisualizerLeds) deviceVisualizerLed.RenderImage(drawingContext); @@ -113,26 +117,18 @@ namespace Artemis.UI.Shared.Controls return rotationRect.Size; } - + private void TimerOnTick(object sender, EventArgs e) + { + if (ShowColors && Visibility == Visibility.Visible) + Render(); + } + private void UpdateTransform() { InvalidateVisual(); InvalidateMeasure(); } - private void SubscribeToUpdate(bool subscribe) - { - if (_subscribed == subscribe) - return; - - if (subscribe) - RGBSurface.Instance.Updated += RgbSurfaceOnUpdated; - else - RGBSurface.Instance.Updated -= RgbSurfaceOnUpdated; - - _subscribed = subscribe; - } - private static void DevicePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { var deviceVisualizer = (DeviceVisualizer) d; @@ -208,17 +204,7 @@ namespace Artemis.UI.Shared.Controls if (e.PropertyName == nameof(Device.RgbDevice.Scale) || e.PropertyName == nameof(Device.RgbDevice.Rotation)) UpdateTransform(); } - - private void RgbSurfaceOnUpdated(UpdatedEventArgs e) - { - _lastRenderTask?.Wait(); - - _lastRenderTask = Dispatcher.InvokeAsync(() => - { - if (ShowColors && Visibility == Visibility.Visible) - Render(); - }).Task; - } + private void Render() { diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index e0599504b..2ddde77e9 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -44,7 +44,7 @@ namespace Artemis.UI.Shared.Controls var r = Led.RgbLed.Color.GetR(); var g = Led.RgbLed.Color.GetG(); var b = Led.RgbLed.Color.GetB(); - + drawingContext.DrawRectangle(isDimmed ? new SolidColorBrush(Color.FromArgb(100, r, g, b)) : new SolidColorBrush(Color.FromRgb(r, g, b)), null, LedRect); diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs index c1fa98e4e..c89aed716 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs @@ -74,5 +74,13 @@ namespace Artemis.UI.Shared.Services.Interfaces PropertyInputRegistration RegisterPropertyInput(PluginInfo pluginInfo) where T : PropertyInputViewModel; void RemovePropertyInput(PropertyInputRegistration registration); + + /// + /// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a segment end. + /// + /// + /// How close the time must be to snap + /// + TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, bool snapToKeyframes); } } \ 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 a7a80f022..2920e79cb 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -34,7 +34,7 @@ namespace Artemis.UI.Shared.Services _registeredPropertyEditors = new List(); Kernel = kernel; - PixelsPerSecond = 31; + PixelsPerSecond = 100; } public IKernel Kernel { get; } @@ -48,7 +48,7 @@ namespace Artemis.UI.Shared.Services set { if (_currentTime.Equals(value)) return; - if (value > SelectedProfileElement.TimelineLength) + if (SelectedProfileElement != null && value > SelectedProfileElement.TimelineLength) _currentTime = SelectedProfileElement.TimelineLength; else _currentTime = value; @@ -205,6 +205,47 @@ namespace Artemis.UI.Shared.Services } } + public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, bool snapToKeyframes) + { + if (snapToSegments) + { + // Snap to the end of the start segment + if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.StartSegmentLength.TotalMilliseconds) < tolerance.TotalMilliseconds) + return SelectedProfileElement.StartSegmentLength; + + // Snap to the end of the main segment + var mainSegmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength; + if (Math.Abs(time.TotalMilliseconds - mainSegmentEnd.TotalMilliseconds) < tolerance.TotalMilliseconds) + return mainSegmentEnd; + + // Snap to the end of the end segment (end of the timeline) + if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.TimelineLength.TotalMilliseconds) < tolerance.TotalMilliseconds) + return SelectedProfileElement.TimelineLength; + } + + if (snapToCurrentTime) + { + // Snap to the current time + if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds) + return SelectedProfileElement.StartSegmentLength; + } + + if (snapToKeyframes) + { + // Get all visible keyframes + var keyframes = SelectedProfileElement.GetAllKeyframes() + .Where(k => SelectedProfileElement.IsPropertyGroupExpanded(k.BaseLayerProperty.Parent)) + .ToList(); + + // Find the closest keyframe + var closeKeyframe = keyframes.FirstOrDefault(k => Math.Abs(time.TotalMilliseconds - k.Position.TotalMilliseconds) < tolerance.TotalMilliseconds); + if (closeKeyframe != null) + return closeKeyframe.Position; + } + + return time; + } + public Module GetCurrentModule() { return (Module) SelectedProfile?.PluginInfo.Instance; diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index 86b6758ec..b3a28bd2f 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -1,4 +1,4 @@ - + d:DesignHeight="450" d:DesignWidth="800" + d:DataContext="{d:DesignInstance {x:Type local:DisplayConditionsViewModel}}"> + + + @@ -19,9 +24,7 @@ - - Display conditions - + @@ -31,38 +34,36 @@ - - Play once - + - When conditions no longer met + - When conditions are no longer met, finish the timelines and then stop displaying. + - WAIT FOR FINISH + - When conditions are no longer met, stop displaying immediately. + - SKIP + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 7f0586705..cbf8e7d86 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -1,4 +1,6 @@ -using Artemis.Core.Models.Profile.Conditions; +using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.Conditions; +using Artemis.Storage.Entities.Profile.Abstract; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Events; using Artemis.UI.Shared.Services.Interfaces; @@ -9,6 +11,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions { private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory; private DisplayConditionGroupViewModel _rootGroup; + private RenderProfileElement _renderProfileElement; public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory) { @@ -22,8 +25,16 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions set => SetAndNotify(ref _rootGroup, value); } + public RenderProfileElement RenderProfileElement + { + get => _renderProfileElement; + set => SetAndNotify(ref _renderProfileElement, value); + } + private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e) { + RenderProfileElement = e.RenderProfileElement; + if (e.RenderProfileElement == null) { RootGroup = null; diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index ea35b1e5a..1ec193a50 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -199,26 +199,58 @@ Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" /> - + Start - + Main - + End + + HorizontalAlignment="Right" Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" /> + HorizontalAlignment="Right" /> + HorizontalAlignment="Right" Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" /> + + + + + @@ -320,6 +352,16 @@ + + + Enable start segment + + + + Enable end segment + + + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 9fb2daac5..fc0f33c64 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -94,12 +94,36 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties if (!SetAndNotify(ref _selectedProfileElement, value)) return; NotifyOfPropertyChange(nameof(SelectedLayer)); NotifyOfPropertyChange(nameof(SelectedFolder)); + NotifyOfPropertyChange(nameof(StartSegmentEnabled)); + NotifyOfPropertyChange(nameof(EndSegmentEnabled)); } } public Layer SelectedLayer => SelectedProfileElement as Layer; public Folder SelectedFolder => SelectedProfileElement as Folder; + public bool StartSegmentEnabled + { + get => SelectedProfileElement?.StartSegmentLength != TimeSpan.Zero; + set + { + SelectedProfileElement.StartSegmentLength = value ? TimeSpan.FromSeconds(1) : TimeSpan.Zero; + ProfileEditorService.UpdateSelectedProfileElement(); + NotifyOfPropertyChange(nameof(StartSegmentEnabled)); + } + } + + public bool EndSegmentEnabled + { + get => SelectedProfileElement?.EndSegmentLength != TimeSpan.Zero; + set + { + SelectedProfileElement.EndSegmentLength = value ? TimeSpan.FromSeconds(1) : TimeSpan.Zero; + ProfileEditorService.UpdateSelectedProfileElement(); + NotifyOfPropertyChange(nameof(EndSegmentEnabled)); + } + } + public BindableCollection LayerPropertyGroups { get => _layerPropertyGroups; @@ -123,7 +147,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties get => _timelineViewModel; set => SetAndNotify(ref _timelineViewModel, value); } - + protected override void OnInitialActivate() { PopulateProperties(ProfileEditorService.SelectedProfileElement); @@ -159,14 +183,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e) { - // Placeholder - if (e.RenderProfileElement != null) - { - e.RenderProfileElement.StartSegmentLength = TimeSpan.FromMilliseconds(1000); - e.RenderProfileElement.MainSegmentLength = TimeSpan.FromMilliseconds(5000); - e.RenderProfileElement.EndSegmentLength = TimeSpan.FromMilliseconds(1000); - } - PopulateProperties(e.RenderProfileElement); } @@ -176,7 +192,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties NotifyOfPropertyChange(nameof(TimeCaretPosition)); } - private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e) { NotifyOfPropertyChange(nameof(TimeCaretPosition)); @@ -538,18 +553,23 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties else newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds)); - if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) + // If holding down shift, snap to the closest segment or keyframe + if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) { - ProfileEditorService.CurrentTime = newTime; + var snappedTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), true, false, true); + ProfileEditorService.CurrentTime = snappedTime; return; } - var visibleKeyframes = GetKeyframes(true); + // If holding down control, round to the closest 50ms + if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) + { + var roundedTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0); + ProfileEditorService.CurrentTime = roundedTime; + return; + } - // Take a tolerance of 5 pixels (half a keyframe width) - var tolerance = 1000f / ProfileEditorService.PixelsPerSecond * 5; - var closeKeyframe = visibleKeyframes.FirstOrDefault(k => Math.Abs(k.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance); - ProfileEditorService.CurrentTime = closeKeyframe?.Position ?? newTime; + ProfileEditorService.CurrentTime = newTime; } } @@ -563,5 +583,98 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties } #endregion + + #region Segments + + private bool _draggingStartSegment; + private bool _draggingMainSegment; + private bool _draggingEndSegment; + + public void StartSegmentMouseDown(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).CaptureMouse(); + _draggingStartSegment = true; + } + + public void StartSegmentMouseUp(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).ReleaseMouseCapture(); + _draggingStartSegment = false; + } + + public void MainSegmentMouseDown(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).CaptureMouse(); + _draggingMainSegment = true; + } + + public void MainSegmentMouseUp(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).ReleaseMouseCapture(); + _draggingMainSegment = false; + } + + public void EndSegmentMouseDown(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).CaptureMouse(); + _draggingEndSegment = true; + } + + public void EndSegmentMouseUp(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).ReleaseMouseCapture(); + _draggingEndSegment = false; + } + + public void SegmentMouseMove(object sender, MouseEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed) + { + // 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 / ProfileEditorService.PixelsPerSecond); + + // Round the time to something that fits the current zoom level + if (ProfileEditorService.PixelsPerSecond < 200) + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0); + else if (ProfileEditorService.PixelsPerSecond < 500) + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0); + else + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds)); + + // If holding down shift, snap to the closest element on the timeline + if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) + { + newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, true); + } + // If holding down control, round to the closest 50ms + else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) + { + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0); + } + + if (_draggingStartSegment) + { + if (newTime < TimeSpan.FromMilliseconds(100)) + newTime = TimeSpan.FromMilliseconds(100); + SelectedProfileElement.StartSegmentLength = newTime; + } + else if (_draggingMainSegment) + { + if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100)) + newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100); + SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength; + } + else if (_draggingEndSegment) + { + if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100)) + newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100); + SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength; + } + } + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/Controls/PropertyTimelineHeader.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/Controls/PropertyTimelineHeader.cs index 22490ddce..3b96466a7 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/Controls/PropertyTimelineHeader.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/Controls/PropertyTimelineHeader.cs @@ -145,10 +145,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline subds = new object[] {6d, 6d, 30d}; else if (PixelsPerSecond > 150) subds = new object[] {4d, 4d, 20d}; - else if (PixelsPerSecond > 100) - subds = new object[] {4d, 4d, 8d}; + else if (PixelsPerSecond > 140) + subds = new object[] {4d, 4d, 20d}; else if (PixelsPerSecond > 90) - subds = new object[] {4d, 4d, 8d}; + subds = new object[] {2d, 4d, 20d}; else if (PixelsPerSecond > 60) subds = new object[] {2d, 4d, 8d}; else if (PixelsPerSecond > 40) diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs index 8e320fd77..226c88150 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs @@ -119,29 +119,51 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline #region Movement - private bool _movementReleased = true; - private TimeSpan _startOffset; + private TimeSpan? _offset; public void ApplyMovement(TimeSpan cursorTime) { - if (_movementReleased) - { - _movementReleased = false; - _startOffset = cursorTime - BaseLayerPropertyKeyframe.Position; - } - else - { - BaseLayerPropertyKeyframe.Position = cursorTime - _startOffset; - if (BaseLayerPropertyKeyframe.Position < TimeSpan.Zero) - BaseLayerPropertyKeyframe.Position = TimeSpan.Zero; - - Update(_pixelsPerSecond); - } + UpdatePosition(cursorTime); + Update(_pixelsPerSecond); } public void ReleaseMovement() { - _movementReleased = true; + _offset = null; + } + + public void SaveOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel) + { + if (keyframeViewModel == this) + { + _offset = null; + return; + } + + if (_offset != null) + return; + + _offset = BaseLayerPropertyKeyframe.Position - keyframeViewModel.BaseLayerPropertyKeyframe.Position; + } + + public void ApplyOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel) + { + if (keyframeViewModel == this || _offset == null) + return; + + UpdatePosition(keyframeViewModel.BaseLayerPropertyKeyframe.Position + _offset.Value); + } + + private void UpdatePosition(TimeSpan position) + { + if (position < TimeSpan.Zero) + BaseLayerPropertyKeyframe.Position = TimeSpan.Zero; + else if (position > _profileEditorService.SelectedProfileElement.TimelineLength) + BaseLayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength; + else + BaseLayerPropertyKeyframe.Position = position; + + Update(_pixelsPerSecond); } #endregion diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml index 3878820b2..ecc2c3c42 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml @@ -13,7 +13,8 @@ Background="{DynamicResource MaterialDesignToolBarBackground}" MouseDown="{s:Action TimelineCanvasMouseDown}" MouseUp="{s:Action TimelineCanvasMouseUp}" - MouseMove="{s:Action TimelineCanvasMouseMove}"> + MouseMove="{s:Action TimelineCanvasMouseMove}" + Margin="0 0 -1 0"> @@ -32,7 +33,7 @@ @@ -48,7 +49,8 @@ X2="{Binding StartSegmentEndPosition}" Y1="0" Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" - HorizontalAlignment="Left" /> + HorizontalAlignment="Left" + Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/> + Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" + Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/> StartSegmentWidth + MainSegmentWidth; public double EndSegmentWidth => _profileEditorService.PixelsPerSecond * _profileEditorService.SelectedProfileElement?.EndSegmentLength.TotalSeconds ?? 0; public double EndSegmentEndPosition => StartSegmentWidth + MainSegmentWidth + EndSegmentWidth; + public double TotalTimelineWidth => _profileEditorService.PixelsPerSecond * _profileEditorService.SelectedProfileElement?.TimelineLength.TotalSeconds ?? 0; + + public bool StartSegmentEnabled => _profileEditorService.SelectedProfileElement?.StartSegmentLength != TimeSpan.Zero; + public bool EndSegmentEnabled => _profileEditorService.SelectedProfileElement?.EndSegmentLength != TimeSpan.Zero; public void Dispose() { _profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; + _profileEditorService.SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged; + } + + private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.StartSegmentLength)) + { + NotifyOfPropertyChange(nameof(StartSegmentWidth)); + NotifyOfPropertyChange(nameof(StartSegmentEndPosition)); + NotifyOfPropertyChange(nameof(MainSegmentEndPosition)); + NotifyOfPropertyChange(nameof(EndSegmentEndPosition)); + NotifyOfPropertyChange(nameof(StartSegmentEnabled)); + NotifyOfPropertyChange(nameof(TotalTimelineWidth)); + } + else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.MainSegmentLength)) + { + NotifyOfPropertyChange(nameof(MainSegmentWidth)); + NotifyOfPropertyChange(nameof(MainSegmentEndPosition)); + NotifyOfPropertyChange(nameof(EndSegmentEndPosition)); + NotifyOfPropertyChange(nameof(TotalTimelineWidth)); + } + else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.EndSegmentLength)) + { + NotifyOfPropertyChange(nameof(EndSegmentWidth)); + NotifyOfPropertyChange(nameof(EndSegmentEndPosition)); + NotifyOfPropertyChange(nameof(EndSegmentEnabled)); + NotifyOfPropertyChange(nameof(TotalTimelineWidth)); + } } public void Update() @@ -74,6 +108,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline NotifyOfPropertyChange(nameof(MainSegmentEndPosition)); NotifyOfPropertyChange(nameof(EndSegmentWidth)); NotifyOfPropertyChange(nameof(EndSegmentEndPosition)); + NotifyOfPropertyChange(nameof(TotalTimelineWidth)); } #region Command handlers @@ -108,8 +143,12 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline public void KeyframeMouseMove(object sender, MouseEventArgs e) { + var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel; + if (viewModel == null) + return; + if (e.LeftButton == MouseButtonState.Pressed) - MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View))); + MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)), viewModel); e.Handled = true; } @@ -161,6 +200,8 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline var tolerance = 1000f / _profileEditorService.PixelsPerSecond * 5; if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - time.TotalMilliseconds) < tolerance) time = _profileEditorService.CurrentTime; + else if (Math.Abs(_profileEditorService.SelectedProfileElement.StartSegmentLength.TotalMilliseconds - time.TotalMilliseconds) < tolerance) + time = _profileEditorService.SelectedProfileElement.StartSegmentLength; } return time; @@ -170,14 +211,19 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline #region Keyframe movement - public void MoveSelectedKeyframes(TimeSpan cursorTime) + public void MoveSelectedKeyframes(TimeSpan cursorTime, TimelineKeyframeViewModel sourceKeyframeViewModel) { // Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging SelectionRectangle.Rect = new Rect(); var keyframeViewModels = GetAllKeyframeViewModels(); foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected)) - keyframeViewModel.ApplyMovement(cursorTime); + keyframeViewModel.SaveOffsetToKeyframe(sourceKeyframeViewModel); + + sourceKeyframeViewModel.ApplyMovement(cursorTime); + + foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected)) + keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel); _layerPropertiesViewModel.ProfileEditorService.UpdateProfilePreview(); }