From 89beb929358588ab1315d7cabb74754feb235908 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 4 Feb 2022 00:15:00 +0100 Subject: [PATCH] Profiles - Removed the technical concept of extra timelines for events Profiles - Properly separated out update and render logic Profile editor - Implemented layer visualization --- .../Profile/Conditions/EventCondition.cs | 4 +- src/Artemis.Core/Models/Profile/Folder.cs | 65 ++--- src/Artemis.Core/Models/Profile/Layer.cs | 243 ++++++++++-------- .../Profile/LayerProperties/LayerProperty.cs | 6 + .../Models/Profile/RenderProfileElement.cs | 11 +- src/Artemis.Core/Models/Profile/Timeline.cs | 127 +++------ .../Services/ProfileEditorService.cs | 3 +- .../ProfileEditor/ProfileEditorService.cs | 6 +- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 3 + .../Ninject/Factories/IVMFactory.cs | 1 + .../VisualEditor/VisualEditorView.axaml | 6 + .../VisualEditor/VisualEditorViewModel.cs | 36 ++- .../Visualizers/IVisualizerViewModel.cs | 3 + .../LayerShapeVisualizerView.axaml | 48 ++++ .../LayerShapeVisualizerView.axaml.cs | 18 ++ .../LayerShapeVisualizerViewModel.cs | 78 ++++++ .../Visualizers/LayerVisualizerView.axaml | 15 +- .../Visualizers/LayerVisualizerViewModel.cs | 50 +--- 18 files changed, 431 insertions(+), 292 deletions(-) create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index 0ba1f5a86..cd4d99794 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -158,8 +158,8 @@ namespace Artemis.Core // If the timeline was already running, look at the event overlap mode if (EventOverlapMode == TimeLineEventOverlapMode.Restart) timeline.JumpToStart(); - else if (EventOverlapMode == TimeLineEventOverlapMode.Copy) - timeline.AddExtraTimeline(); + else if (EventOverlapMode == TimeLineEventOverlapMode.Copy && ProfileElement is Layer layer) + layer.CreateCopyAsChild(); else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet) timeline.JumpToStart(); diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 52296fb74..dd00ed756 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -102,6 +102,9 @@ namespace Artemis.Core else if (Timeline.IsFinished) Disable(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + foreach (ProfileElement child in Children) child.Update(deltaTime); } @@ -176,41 +179,35 @@ namespace Artemis.Core // No point rendering if all children are disabled if (!Children.Any(c => c is RenderProfileElement {Enabled: true})) return; - - lock (Timeline) + + SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; + try { - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalUpdate(Timeline); + SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint); - SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; - try - { - SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint); + // No point rendering if the alpha was set to zero by one of the effects + if (layerPaint.Color.Alpha == 0) + return; - // No point rendering if the alpha was set to zero by one of the effects - if (layerPaint.Color.Alpha == 0) - return; + canvas.SaveLayer(layerPaint); + canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); - canvas.SaveLayer(layerPaint); - canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); + // Iterate the children in reverse because the first layer must be rendered last to end up on top + for (int index = Children.Count - 1; index > -1; index--) + Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top)); - // Iterate the children in reverse because the first layer must be rendered last to end up on top - for (int index = Children.Count - 1; index > -1; index--) - Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top)); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint); - } - finally - { - canvas.Restore(); - layerPaint.DisposeSelfAndProperties(); - } - - Timeline.ClearDelta(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint); } + finally + { + canvas.Restore(); + layerPaint.DisposeSelfAndProperties(); + } + + Timeline.ClearDelta(); } #endregion @@ -233,6 +230,14 @@ namespace Artemis.Core Enabled = true; } + /// + public override void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment) + { + Timeline.Override(position, stickToMainSegment); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + } + /// public override void Disable() { @@ -314,7 +319,7 @@ namespace Artemis.Core ChildrenList.Add(new Layer(Profile, this, childLayer)); // Ensure order integrity, should be unnecessary but no one is perfect specially me - ChildrenList.Sort((a,b) => a.Order.CompareTo(b.Order)); + ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order)); for (int index = 0; index < ChildrenList.Count; index++) ChildrenList[index].Order = index + 1; diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 4eadc28c6..8b87d2fc5 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -345,17 +345,30 @@ namespace Artemis.Core Enable(); else if (Timeline.IsFinished) Disable(); - } - /// - public override void Reset() - { - UpdateDisplayCondition(); + if (Timeline.Delta == TimeSpan.Zero) + return; - if (DisplayConditionMet) - Timeline.JumpToStart(); - else - Timeline.JumpToEnd(); + General.Update(Timeline); + Transform.Update(Timeline); + LayerBrush?.InternalUpdate(Timeline); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + + // Remove children that finished their timeline and update the rest + for (int index = 0; index < Children.Count; index++) + { + ProfileElement profileElement = Children[index]; + if (((Layer) profileElement).Timeline.IsFinished) + { + RemoveChild(profileElement); + profileElement.Dispose(); + index--; + } + else + profileElement.Update(deltaTime); + } } /// @@ -365,88 +378,18 @@ namespace Artemis.Core throw new ObjectDisposedException("Layer"); // Ensure the layer is ready - if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized) + if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized || !Leds.Any()) return; + + // Render children first so they go below + for (int i = Children.Count - 1; i >= 0; i--) + Children[i].Render(canvas, basePosition); + // Ensure the brush is ready if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false) return; - RenderTimeline(Timeline, canvas, basePosition); - foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList()) - RenderTimeline(extraTimeline, canvas, basePosition); - Timeline.ClearDelta(); - } - - /// - public override void Enable() - { - if (Enabled) - return; - - bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush"); - if (!tryOrBreak) - return; - - tryOrBreak = TryOrBreak(() => - { - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.InternalEnable(); - }, "Failed to enable one or more effects"); - if (!tryOrBreak) - return; - - Enabled = true; - } - - /// - public override void Activate() - { - throw new NotImplementedException(); - } - - /// - public override void Deactivate() - { - throw new NotImplementedException(); - } - - /// - public override void Disable() - { - if (!Enabled) - return; - - LayerBrush?.InternalDisable(); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.InternalDisable(); - - Enabled = false; - } - - private void ApplyTimeline(Timeline timeline) - { - if (timeline.Delta == TimeSpan.Zero) - return; - - General.Update(timeline); - Transform.Update(timeline); - LayerBrush?.InternalUpdate(timeline); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalUpdate(timeline); - } - - private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPointI basePosition) - { - if (Path == null || LayerBrush == null) - throw new ArtemisCoreException("The layer is not yet ready for rendering"); - - if (!Leds.Any() || timeline.IsFinished) - return; - - ApplyTimeline(timeline); - - if (LayerBrush?.BrushType != LayerBrushType.Regular) + if (Timeline.IsFinished || LayerBrush?.BrushType != LayerBrushType.Regular) return; SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; @@ -501,34 +444,96 @@ namespace Artemis.Core canvas.Restore(); layerPaint.DisposeSelfAndProperties(); } + + Timeline.ClearDelta(); } - private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) + /// + public override void Enable() { - if (LayerBrush == null) - throw new ArtemisCoreException("The layer is not yet ready for rendering"); + if (Enabled) + return; + + bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush"); + if (!tryOrBreak) + return; + + tryOrBreak = TryOrBreak(() => + { + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.InternalEnable(); + }, "Failed to enable one or more effects"); + if (!tryOrBreak) + return; + + Enabled = true; + } + + /// + public override void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment) + { + Timeline.Override(position, stickToMainSegment); + + General.Update(Timeline); + Transform.Update(Timeline); + LayerBrush?.InternalUpdate(Timeline); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); + baseLayerEffect.InternalUpdate(Timeline); + } - try + /// + public override void Activate() + { + throw new NotImplementedException(); + } + + /// + public override void Deactivate() + { + throw new NotImplementedException(); + } + + /// + public override void Disable() + { + if (!Enabled) + return; + + LayerBrush?.InternalDisable(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.InternalDisable(); + + Enabled = false; + } + + /// + public override void Reset() + { + UpdateDisplayCondition(); + + if (DisplayConditionMet) + Timeline.JumpToStart(); + else + Timeline.JumpToEnd(); + + while (Children.Any()) { - canvas.SaveLayer(layerPaint); - canvas.ClipPath(renderPath); - - // Restore the blend mode before doing the actual render - layerPaint.BlendMode = SKBlendMode.SrcOver; - - LayerBrush.InternalRender(canvas, bounds, layerPaint); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); + Children[0].Dispose(); + RemoveChild(Children[0]); } + } - finally - { - canvas.Restore(); - } + /// + /// Creates a copy of this layer as a child and plays it once + /// + public void CreateCopyAsChild() + { + throw new NotImplementedException(); + + // Create a copy of the layer and it's properties + + // Add to children } internal void CalculateRenderProperties() @@ -582,6 +587,34 @@ namespace Artemis.Core return position; } + private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) + { + if (LayerBrush == null) + throw new ArtemisCoreException("The layer is not yet ready for rendering"); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); + + try + { + canvas.SaveLayer(layerPaint); + canvas.ClipPath(renderPath); + + // Restore the blend mode before doing the actual render + layerPaint.BlendMode = SKBlendMode.SrcOver; + + LayerBrush.InternalRender(canvas, bounds, layerPaint); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); + } + + finally + { + canvas.Restore(); + } + } + /// /// Creates a transformation matrix that applies the current transformation settings /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 1abd9ecba..6bdf1fd6d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -49,6 +49,12 @@ namespace Artemis.Core Keyframes = new ReadOnlyCollection>(_keyframes); } + /// + public override string ToString() + { + return $"{Path} - {CurrentValue} ({PropertyType})"; + } + /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index be2ec4a2b..85aec916d 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -137,7 +137,7 @@ namespace Artemis.Core /// Updates the according to the provided and current display /// condition status /// - public void UpdateTimeline(double deltaTime) + protected void UpdateTimeline(double deltaTime) { // TODO: Move to conditions @@ -327,7 +327,7 @@ namespace Artemis.Core OrderEffects(); } - + internal void ActivateLayerEffect(BaseLayerEffect layerEffect) { @@ -406,5 +406,12 @@ namespace Artemis.Core } #endregion + + /// + /// Overrides the main timeline to the specified time and clears any extra time lines + /// + /// The position to set the timeline to + /// Whether to stick to the main segment, wrapping around if needed + public abstract void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs index ef4bf2740..4e17d746f 100644 --- a/src/Artemis.Core/Models/Profile/Timeline.cs +++ b/src/Artemis.Core/Models/Profile/Timeline.cs @@ -11,7 +11,6 @@ namespace Artemis.Core; /// public class Timeline : CorePropertyChanged, IStorageModel { - private const int MaxExtraTimelines = 15; private readonly object _lock = new(); /// @@ -21,78 +20,33 @@ public class Timeline : CorePropertyChanged, IStorageModel { Entity = new TimelineEntity(); MainSegmentLength = TimeSpan.FromSeconds(5); - - _extraTimelines = new List(); - ExtraTimelines = new ReadOnlyCollection(_extraTimelines); - + Save(); } internal Timeline(TimelineEntity entity) { Entity = entity; - _extraTimelines = new List(); - ExtraTimelines = new ReadOnlyCollection(_extraTimelines); Load(); } - private Timeline(Timeline parent) - { - Entity = new TimelineEntity(); - Parent = parent; - StartSegmentLength = Parent.StartSegmentLength; - MainSegmentLength = Parent.MainSegmentLength; - EndSegmentLength = Parent.EndSegmentLength; - - _extraTimelines = new List(); - ExtraTimelines = new ReadOnlyCollection(_extraTimelines); - } - /// public override string ToString() { return $"Progress: {Position}/{Length} - delta: {Delta}"; } - - #region Extra timelines - - /// - /// Adds an extra timeline to this timeline - /// - public void AddExtraTimeline() - { - _extraTimelines.Add(new Timeline(this)); - if (_extraTimelines.Count > MaxExtraTimelines) - _extraTimelines.RemoveAt(0); - } - - /// - /// Removes all extra timelines from this timeline - /// - public void ClearExtraTimelines() - { - _extraTimelines.Clear(); - } - - #endregion - + #region Properties private TimeSpan _position; private TimeSpan _lastDelta; private TimelinePlayMode _playMode; private TimelineStopMode _stopMode; - private readonly List _extraTimelines; private TimeSpan _startSegmentLength; private TimeSpan _mainSegmentLength; private TimeSpan _endSegmentLength; - - /// - /// Gets the parent this timeline is an extra timeline of - /// - public Timeline? Parent { get; } - + /// /// Gets the current position of the timeline /// @@ -105,21 +59,13 @@ public class Timeline : CorePropertyChanged, IStorageModel /// /// Gets the cumulative delta of all calls to that took place after the last call to /// - /// - /// Note: If this is an extra timeline is always equal to - /// /// public TimeSpan Delta { - get => Parent == null ? _lastDelta : DeltaToParent; + get => _lastDelta; private set => SetAndNotify(ref _lastDelta, value); } - - /// - /// Gets the delta to this timeline's - /// - public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero; - + /// /// Gets or sets the mode in which the render element starts its timeline when display conditions are met /// @@ -138,15 +84,10 @@ public class Timeline : CorePropertyChanged, IStorageModel set => SetAndNotify(ref _stopMode, value); } - /// - /// Gets a list of extra copies of the timeline applied to this timeline - /// - public ReadOnlyCollection ExtraTimelines { get; } - /// /// Gets a boolean indicating whether the timeline has finished its run /// - public bool IsFinished => Position > Length && !ExtraTimelines.Any(); + public bool IsFinished => Position > Length; /// /// Gets a boolean indicating whether the timeline progress has been overridden @@ -327,19 +268,15 @@ public class Timeline : CorePropertyChanged, IStorageModel IsOverridden = false; _lastOverridePosition = Position; - if (stickToMainSegment && Position > MainSegmentEndPosition) - { - // If the main segment has no length, simply stick to the start of the segment - if (MainSegmentLength == TimeSpan.Zero) - Position = MainSegmentStartPosition; - // Ensure wrapping back around retains the delta time - else - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); - } + if (!stickToMainSegment || Position <= MainSegmentEndPosition) + return; - _extraTimelines.RemoveAll(t => t.IsFinished); - foreach (Timeline extraTimeline in _extraTimelines) - extraTimeline.Update(delta, false); + // If the main segment has no length, simply stick to the start of the segment + if (MainSegmentLength == TimeSpan.Zero) + Position = MainSegmentStartPosition; + // Ensure wrapping back around retains the delta time + else + Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); } } @@ -389,11 +326,11 @@ public class Timeline : CorePropertyChanged, IStorageModel } /// - /// Overrides the to the specified time and clears any extra time lines + /// Overrides the to the specified time /// /// The position to set the timeline to /// Whether to stick to the main segment, wrapping around if needed - public void Override(TimeSpan position, bool stickToMainSegment) + internal void Override(TimeSpan position, bool stickToMainSegment) { lock (_lock) { @@ -403,24 +340,22 @@ public class Timeline : CorePropertyChanged, IStorageModel IsOverridden = true; _lastOverridePosition = position; - if (stickToMainSegment && Position >= MainSegmentStartPosition) - { - bool atSegmentStart = Position == MainSegmentStartPosition; - if (MainSegmentLength > TimeSpan.Zero) - { - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); - // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor - // is actually at the start of the segment - if (Position == MainSegmentStartPosition && !atSegmentStart) - Position = MainSegmentEndPosition; - } - else - { - Position = MainSegmentStartPosition; - } - } + if (!stickToMainSegment || Position < MainSegmentStartPosition) + return; - _extraTimelines.Clear(); + bool atSegmentStart = Position == MainSegmentStartPosition; + if (MainSegmentLength > TimeSpan.Zero) + { + Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); + // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor + // is actually at the start of the segment + if (Position == MainSegmentStartPosition && !atSegmentStart) + Position = MainSegmentEndPosition; + } + else + { + Position = MainSegmentStartPosition; + } } } diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 19118d88f..68d1782f0 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -146,10 +146,11 @@ namespace Artemis.UI.Shared.Services else { renderElement.Enable(); - renderElement.Timeline.Override( + renderElement.OverrideTimelineAndApply( CurrentTime, (renderElement != SelectedProfileElement || renderElement.Timeline.Length < CurrentTime) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat ); + renderElement.Update(0); foreach (ProfileElement child in renderElement.Children) TickProfileElement(child); diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 1c52211dd..858fb5427 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -79,10 +79,8 @@ internal class ProfileEditorService : IProfileEditorService else { renderElement.Enable(); - renderElement.Timeline.Override( - time, - (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat - ); + bool stickToMainSegment = (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat; + renderElement.OverrideTimelineAndApply(time, stickToMainSegment); foreach (ProfileElement child in renderElement.Children) TickProfileElement(child, time); diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 0de0c2b5d..f53a9a426 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -56,6 +56,9 @@ PropertiesView.axaml + + LayerShapeVisualizerView.axaml + diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 7f7eaa297..b7163bcd3 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -65,6 +65,7 @@ namespace Artemis.UI.Ninject.Factories ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder); LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer); + LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer); LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer); } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml index dcf5a3b7f..dfa802ef5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml @@ -48,6 +48,12 @@ + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 5169bca53..1ad1fa430 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive.Disposables; using Artemis.Core; @@ -8,6 +9,8 @@ using Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; +using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor; @@ -16,14 +19,22 @@ public class VisualEditorViewModel : ActivatableViewModelBase { private readonly IProfileEditorVmFactory _vmFactory; private ObservableAsPropertyHelper? _profileConfiguration; + private readonly SourceList _visualizers; public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory) { _vmFactory = vmFactory; + _visualizers = new SourceList(); + Devices = new ObservableCollection(rgbService.EnabledDevices); - Visualizers = new ObservableCollection(); Tools = new ObservableCollection(); + _visualizers.Connect() + .Sort(SortExpressionComparer.Ascending(vm => vm.Order)) + .Bind(out ReadOnlyObservableCollection visualizers) + .Subscribe(); + Visualizers = visualizers; + this.WhenActivated(d => { _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); @@ -34,21 +45,24 @@ public class VisualEditorViewModel : ActivatableViewModelBase public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public ObservableCollection Devices { get; } - public ObservableCollection Visualizers { get; set; } - public ObservableCollection Tools { get; set; } + public ReadOnlyObservableCollection Visualizers { get; } + public ObservableCollection Tools { get; } private void CreateVisualizers(ProfileConfiguration? profileConfiguration) { - Visualizers.Clear(); - if (profileConfiguration?.Profile == null) - return; - - foreach (Layer layer in profileConfiguration.Profile.GetAllLayers()) - CreateVisualizer(layer); + _visualizers.Edit(list => + { + list.Clear(); + if (profileConfiguration?.Profile == null) + return; + foreach (Layer layer in profileConfiguration.Profile.GetAllLayers()) + CreateVisualizer(list, layer); + }); } - private void CreateVisualizer(Layer layer) + private void CreateVisualizer(ICollection visualizerViewModels, Layer layer) { - Visualizers.Add(_vmFactory.LayerVisualizerViewModel(layer)); + visualizerViewModels.Add(_vmFactory.LayerShapeVisualizerViewModel(layer)); + visualizerViewModels.Add(_vmFactory.LayerVisualizerViewModel(layer)); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs index 843d32114..385fab60f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs @@ -2,4 +2,7 @@ public interface IVisualizerViewModel { + int X { get; } + int Y { get; } + int Order { get; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml new file mode 100644 index 000000000..0fe4b62bb --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs new file mode 100644 index 000000000..cbef48437 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers +{ + public partial class LayerShapeVisualizerView : ReactiveUserControl + { + public LayerShapeVisualizerView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs new file mode 100644 index 000000000..ea3baee08 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs @@ -0,0 +1,78 @@ +using System; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; +using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia; +using Avalonia.Controls.Mixins; +using Avalonia.Media; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; + +public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel +{ + private ObservableAsPropertyHelper? _selected; + private Geometry? _shapeGeometry; + + public LayerShapeVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService) + { + Layer = layer; + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x).Subscribe(_ => Update()).DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.Position.CurrentValueSet += x, x => Layer.Transform.Position.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.Rotation.CurrentValueSet += x, x => Layer.Transform.Rotation.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.Scale.CurrentValueSet += x, x => Layer.Transform.Scale.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.AnchorPoint.CurrentValueSet += x, x => Layer.Transform.AnchorPoint.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + + _selected = profileEditorService.ProfileElement.Select(p => p == Layer).ToProperty(this, vm => vm.Selected).DisposeWith(d); + + profileEditorService.Time.Subscribe(_ => UpdateTransform()).DisposeWith(d); + Update(); + UpdateTransform(); + }); + } + + public Layer Layer { get; } + public bool Selected => _selected?.Value ?? false; + public Rect LayerBounds => Layer.Bounds.ToRect(); + + public Geometry? ShapeGeometry + { + get => _shapeGeometry; + set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value); + } + + private void Update() + { + if (Layer.General.ShapeType.CurrentValue == LayerShapeType.Rectangle) + ShapeGeometry = new RectangleGeometry(new Rect(0, 0, Layer.Bounds.Width, Layer.Bounds.Height)); + else + ShapeGeometry = new EllipseGeometry(new Rect(0, 0, Layer.Bounds.Width, Layer.Bounds.Height)); + + this.RaisePropertyChanged(nameof(X)); + this.RaisePropertyChanged(nameof(Y)); + this.RaisePropertyChanged(nameof(LayerBounds)); + } + + private void UpdateTransform() + { + if (ShapeGeometry != null) + ShapeGeometry.Transform = new MatrixTransform(Layer.GetTransformMatrix(true, true, true, true).ToMatrix()); + } + + public int X => Layer.Bounds.Left; + public int Y => Layer.Bounds.Top; + public int Order => 2; +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml index ec8a92ae9..6e24c808a 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml @@ -5,19 +5,24 @@ xmlns:visualizers="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerVisualizerView" - x:DataType="visualizers:LayerVisualizerViewModel"> + x:DataType="visualizers:LayerVisualizerViewModel" + ZIndex="1"> + Margin="0 0 2 2" + StrokeDashArray="6,2" + StrokeJoin="Round"> + + + - \ No newline at end of file + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs index d74e985cb..aa5c84eab 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs @@ -2,66 +2,44 @@ using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared; -using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia; using Avalonia.Controls.Mixins; -using Avalonia.Media; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel { - private Geometry? _shapeGeometry; private ObservableAsPropertyHelper? _selected; public LayerVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService) { Layer = layer; - this.WhenActivated(d => { - Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x).Subscribe(_ => UpdateShape()).DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.Position.CurrentValueSet += x, x => Layer.Transform.Position.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) + Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x) + .Subscribe(_ => Update()) .DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.Rotation.CurrentValueSet += x, x => Layer.Transform.Rotation.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) + _selected = profileEditorService.ProfileElement + .Select(p => p == Layer) + .ToProperty(this, vm => vm.Selected) .DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.Scale.CurrentValueSet += x, x => Layer.Transform.Scale.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) - .DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.AnchorPoint.CurrentValueSet += x, x => Layer.Transform.AnchorPoint.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) - .DisposeWith(d); - - _selected = profileEditorService.ProfileElement.Select(p => p == Layer).ToProperty(this, vm => vm.Selected).DisposeWith(d); - - profileEditorService.Time.Subscribe(_ => UpdateTransform()).DisposeWith(d); - UpdateShape(); }); } public Layer Layer { get; } public bool Selected => _selected?.Value ?? false; + public Rect LayerBounds => new(0, 0, Layer.Bounds.Width, Layer.Bounds.Height); - public Geometry? ShapeGeometry + private void Update() { - get => _shapeGeometry; - set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value); + this.RaisePropertyChanged(nameof(X)); + this.RaisePropertyChanged(nameof(Y)); + this.RaisePropertyChanged(nameof(LayerBounds)); } - private void UpdateShape() - { - if (Layer.General.ShapeType.CurrentValue == LayerShapeType.Rectangle) - ShapeGeometry = new RectangleGeometry(Layer.Bounds.ToRect()); - else - ShapeGeometry = new EllipseGeometry(Layer.Bounds.ToRect()); - } - - private void UpdateTransform() - { - if (ShapeGeometry != null) - ShapeGeometry.Transform = new MatrixTransform(Layer.GetTransformMatrix(false, true, true, true).ToMatrix()); - } + public int X => Layer.Bounds.Left; + public int Y => Layer.Bounds.Top; + public int Order => 1; } \ No newline at end of file