mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Profile editor - Implemented timeline segments
Profile editor - Added snapping to multiple elements of the timeline
This commit is contained in:
parent
c5219bd224
commit
527fef3dc6
@ -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<BaseLayerEffect>();
|
||||
_expandedPropertyGroups = new List<string>();
|
||||
ApplyRenderElementDefaults();
|
||||
}
|
||||
|
||||
internal Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity)
|
||||
@ -42,8 +44,6 @@ namespace Artemis.Core.Models.Profile
|
||||
_expandedPropertyGroups = new List<string>();
|
||||
_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;
|
||||
|
||||
@ -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<string>();
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override List<BaseLayerPropertyKeyframe> 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<BaseLayerProperty>(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)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all keyframes on all properties and effects of this layer
|
||||
/// </summary>
|
||||
public virtual List<BaseLayerPropertyKeyframe> GetAllKeyframes()
|
||||
{
|
||||
var keyframes = new List<BaseLayerPropertyKeyframe>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current timeline position
|
||||
/// </summary>
|
||||
public TimeSpan TimelinePosition
|
||||
{
|
||||
get => _timelinePosition;
|
||||
private set => SetAndNotify(ref _timelinePosition, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total combined length of all three segments
|
||||
/// </summary>
|
||||
@ -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
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<BaseLayerEffect> 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
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the display conditions applied to this layer where met or not during last update
|
||||
/// <para>Always true if the layer has no display conditions</para>
|
||||
/// </summary>
|
||||
public bool DisplayConditionMet
|
||||
{
|
||||
get => _displayConditionMet;
|
||||
private set => SetAndNotify(ref _displayConditionMet, value);
|
||||
}
|
||||
|
||||
private DisplayConditionGroup _displayConditionGroup;
|
||||
|
||||
private TimeSpan _timelinePosition;
|
||||
private bool _displayConditionMet;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the root display condition group
|
||||
/// </summary>
|
||||
@ -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
|
||||
|
||||
@ -98,7 +98,7 @@ namespace Artemis.Core.Plugins.LayerBrush.Abstract
|
||||
/// <summary>
|
||||
/// Called before rendering every frame, write your update logic here
|
||||
/// </summary>
|
||||
/// <param name="deltaTime"></param>
|
||||
/// <param name="deltaTime">Seconds passed since last update</param>
|
||||
public abstract void Update(double deltaTime);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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<LayerEffectEntity> LayerEffects { get; set; }
|
||||
public List<PropertyEntity> PropertyEntities { get; set; }
|
||||
public List<string> ExpandedPropertyGroups { get; set; }
|
||||
|
||||
@ -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<DeviceVisualizerLed> _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<DeviceVisualizerLed>();
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -74,5 +74,13 @@ namespace Artemis.UI.Shared.Services.Interfaces
|
||||
PropertyInputRegistration RegisterPropertyInput<T>(PluginInfo pluginInfo) where T : PropertyInputViewModel;
|
||||
|
||||
void RemovePropertyInput(PropertyInputRegistration registration);
|
||||
|
||||
/// <summary>
|
||||
/// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a segment end.
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <param name="tolerance">How close the time must be to snap</param>
|
||||
/// <returns></returns>
|
||||
TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, bool snapToKeyframes);
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ namespace Artemis.UI.Shared.Services
|
||||
_registeredPropertyEditors = new List<PropertyInputRegistration>();
|
||||
|
||||
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;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions.DisplayConditionsView"
|
||||
<UserControl
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@ -6,8 +6,13 @@
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:Converters="clr-namespace:Artemis.UI.Converters" x:Class="Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions.DisplayConditionsView"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type local:DisplayConditionsViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<Converters:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@ -19,9 +24,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Margin="10 5 0 -4">
|
||||
Display conditions
|
||||
</TextBlock>
|
||||
<TextBlock Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Margin="10 5 0 -4"><Run Text="Display conditions"/></TextBlock>
|
||||
<Separator Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignDarkSeparator}" Margin="8 0" />
|
||||
|
||||
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
|
||||
@ -31,38 +34,36 @@
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="3" Grid.Column="0" Margin="13" Orientation="Horizontal" VerticalAlignment="Bottom" ToolTip="When conditions are met, only go through the entire timeline once.">
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}">
|
||||
Play once
|
||||
</CheckBox>
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" Content="Play once" IsChecked="{Binding RenderProfileElement.RepeatMainSegment, Converter={StaticResource InverseBooleanConverter}, Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="3" Grid.Column="1" Margin="10" HorizontalAlignment="Right">
|
||||
<TextBlock>When conditions no longer met</TextBlock>
|
||||
<TextBlock><Run Text="When conditions no longer met"/></TextBlock>
|
||||
<ListBox Style="{StaticResource MaterialDesignToolToggleListBox}" SelectedIndex="{Binding CurrentTimelineIndex}" Height="20" Margin="0 5 0 0">
|
||||
<ListBoxItem Padding="10 0">
|
||||
<ListBoxItem.ToolTip>
|
||||
<ToolTip Placement="Top" VerticalOffset="-5">
|
||||
<StackPanel>
|
||||
<TextBlock>When conditions are no longer met, finish the timelines and then stop displaying.</TextBlock>
|
||||
<TextBlock><Run Text="When conditions are no longer met, finish the timelines and then stop displaying."/></TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</ListBoxItem.ToolTip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="PlayArrow" Width="20" Height="20" Margin="0 -4" />
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11">WAIT FOR FINISH</TextBlock>
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11"><Run Text="WAIT FOR FINISH"/></TextBlock>
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem Padding="10 0">
|
||||
<ListBoxItem.ToolTip>
|
||||
<ToolTip Placement="Top" VerticalOffset="-5">
|
||||
<StackPanel>
|
||||
<TextBlock>When conditions are no longer met, stop displaying immediately.</TextBlock>
|
||||
<TextBlock><Run Text="When conditions are no longer met, stop displaying immediately."/></TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</ListBoxItem.ToolTip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="SkipNext" Width="20" Height="20" Margin="0 -4" />
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11">SKIP</TextBlock>
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11"><Run Text="SKIP"/></TextBlock>
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -199,26 +199,58 @@
|
||||
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" />
|
||||
|
||||
<!-- Start segment -->
|
||||
<TextBlock Grid.Column="0" TextAlignment="Center" VerticalAlignment="Top" Padding="0 3" FontSize="11" Background="{StaticResource MaterialDesignPaper}">
|
||||
<TextBlock Grid.Column="0"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Padding="0 3"
|
||||
FontSize="11"
|
||||
Background="{StaticResource MaterialDesignPaper}"
|
||||
Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
ToolTip="This segment is played when a layer starts displaying because it's conditions are met">
|
||||
Start
|
||||
</TextBlock>
|
||||
|
||||
<!-- Main segment -->
|
||||
<TextBlock Grid.Column="1" TextAlignment="Center" VerticalAlignment="Top" Padding="0 3" FontSize="11" Background="{StaticResource MaterialDesignPaper}">
|
||||
<TextBlock Grid.Column="1"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Padding="0 3"
|
||||
FontSize="11"
|
||||
Background="{StaticResource MaterialDesignPaper}"
|
||||
ToolTip="This segment is played while a condition is met, either once or on a repeating loop">
|
||||
Main
|
||||
</TextBlock>
|
||||
|
||||
<!-- End segment -->
|
||||
<TextBlock Grid.Column="2" TextAlignment="Center" VerticalAlignment="Top" Padding="0 3" FontSize="11" Background="{StaticResource MaterialDesignPaper}">
|
||||
<TextBlock Grid.Column="2"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Padding="0 3"
|
||||
FontSize="11"
|
||||
Background="{StaticResource MaterialDesignPaper}"
|
||||
Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
ToolTip="This segment is played once a condition is no longer met">
|
||||
End
|
||||
</TextBlock>
|
||||
|
||||
<!-- Segment movement display -->
|
||||
<Rectangle Grid.Column="0" RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="0 -1 -2 0" Width="5" Height="20" VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" Cursor="SizeWE" />
|
||||
HorizontalAlignment="Right" Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
|
||||
<Rectangle Grid.Column="1" RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="0 -1 -2 0" Width="5" Height="20" VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" Cursor="SizeWE" />
|
||||
HorizontalAlignment="Right" />
|
||||
<Rectangle Grid.Column="2" RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="0 -1 -2 0" Width="5" Height="20" VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" Cursor="SizeWE" />
|
||||
HorizontalAlignment="Right" Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
|
||||
|
||||
<!-- Segment movement handles -->
|
||||
<Rectangle Grid.Column="0" RadiusX="2" RadiusY="2" Fill="Transparent" Margin="0 -1 -6 0" Width="16" Height="20" VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" Cursor="SizeWE" Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
MouseDown="{s:Action StartSegmentMouseDown}" MouseUp="{s:Action StartSegmentMouseUp}" MouseMove="{s:Action SegmentMouseMove}"/>
|
||||
<Rectangle Grid.Column="1" RadiusX="2" RadiusY="2" Fill="Transparent" Margin="0 -1 -6 0" Width="16" Height="20" VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" Cursor="SizeWE"
|
||||
MouseDown="{s:Action MainSegmentMouseDown}" MouseUp="{s:Action MainSegmentMouseUp}" MouseMove="{s:Action SegmentMouseMove}"/>
|
||||
<Rectangle Grid.Column="2" RadiusX="2" RadiusY="2" Fill="Transparent" Margin="0 -1 -6 0" Width="16" Height="20" VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" Cursor="SizeWE" Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
MouseDown="{s:Action EndSegmentMouseDown}" MouseUp="{s:Action EndSegmentMouseUp}" MouseMove="{s:Action SegmentMouseMove}"/>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
@ -320,6 +352,16 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding StartSegmentEnabled}">
|
||||
Enable start segment
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" Margin="10 0" IsChecked="{Binding EndSegmentEnabled}">
|
||||
Enable end segment
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Zoom control -->
|
||||
<Slider Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
@ -329,6 +371,7 @@
|
||||
Maximum="350"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
AutoToolTipPlacement="TopLeft"
|
||||
Value="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||
Width="319" />
|
||||
|
||||
|
||||
@ -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<LayerPropertyGroupViewModel> 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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
<Grid.Triggers>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
|
||||
<BeginStoryboard>
|
||||
@ -32,7 +33,7 @@
|
||||
</Grid.Triggers>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
|
||||
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}}"
|
||||
MinWidth="{Binding TotalTimelineWidth}"
|
||||
HorizontalAlignment="Left">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@ -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}}"/>
|
||||
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
|
||||
Opacity="0.5"
|
||||
StrokeDashArray="4 2"
|
||||
@ -62,7 +64,8 @@
|
||||
X1="{Binding EndSegmentEndPosition}"
|
||||
X2="{Binding EndSegmentEndPosition}"
|
||||
Y1="0"
|
||||
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" />
|
||||
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
|
||||
Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
|
||||
|
||||
<!-- Multi-selection rectangle -->
|
||||
<Path x:Name="MultiSelectionPath"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@ -27,6 +28,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
||||
LayerPropertyGroups = layerPropertyGroups;
|
||||
SelectionRectangle = new RectangleGeometry();
|
||||
|
||||
_profileEditorService.SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
|
||||
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
|
||||
Update();
|
||||
@ -46,10 +48,42 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
||||
public double MainSegmentEndPosition => 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();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user