diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index d467ba655..8c54d17f5 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -149,9 +149,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition { if (TriggerMode == EventTriggerMode.Toggle) { - if (!IsMet && _wasMet) - ProfileElement.Timeline.JumpToEnd(); - else if (IsMet && !_wasMet) + if (IsMet && !_wasMet) ProfileElement.Timeline.JumpToStart(); } else @@ -164,12 +162,13 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition { if (OverlapMode == EventOverlapMode.Restart) ProfileElement.Timeline.JumpToStart(); - else if (OverlapMode == EventOverlapMode.Copy && ProfileElement is Layer layer) - layer.CreateCopyAsChild(); + else if (OverlapMode == EventOverlapMode.Copy && ProfileElement is Layer layer && layer.Parent is not Layer) + layer.CreateRenderCopy(10); } } - ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), TriggerMode == EventTriggerMode.Toggle); + // Stick to mean segment in toggle mode for as long as the condition is met + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), TriggerMode == EventTriggerMode.Toggle && IsMet); } /// @@ -225,10 +224,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == EventDefaultNode.NodeId); if (existingEventNode != null) _eventNode = (EventDefaultNode) existingEventNode; - + UpdateEventNode(); Script.LoadConnections(); - } #endregion diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index c42df2b5b..8ffbaec64 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -190,8 +190,11 @@ namespace Artemis.Core 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); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + if (!baseLayerEffect.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) @@ -204,8 +207,11 @@ namespace Artemis.Core 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); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint); + } } finally { diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 769816e4e..79099830f 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -16,6 +16,7 @@ namespace Artemis.Core /// public sealed class Layer : RenderProfileElement { + private readonly List _renderCopies; private LayerGeneralProperties _general; private BaseLayerBrush? _layerBrush; private LayerShape? _layerShape; @@ -37,6 +38,8 @@ namespace Artemis.Core Name = name; Suspended = false; + // TODO: move to top + _renderCopies = new List(); _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -61,6 +64,8 @@ namespace Artemis.Core Profile = profile; Parent = parent; + // TODO: move to top + _renderCopies = new List(); _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -76,15 +81,15 @@ namespace Artemis.Core /// Creates a new instance of the class by copying the provided . /// /// The layer to copy - /// The parent of the layer - public Layer(Layer source, ProfileElement parent) : base(parent, parent.Profile) + private Layer(Layer source) : base(source, source.Profile) { - LayerEntity = CoreJson.DeserializeObject(CoreJson.SerializeObject(source.LayerEntity, true), true) ?? new LayerEntity(); - LayerEntity.Id = Guid.NewGuid(); + LayerEntity = source.LayerEntity; Profile = source.Profile; - Parent = parent; + Parent = source; + // TODO: move to top + _renderCopies = new List(); _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -94,6 +99,13 @@ namespace Artemis.Core Adapter = new LayerAdapter(this); Load(); Initialize(); + + Timeline.JumpToStart(); + AddLeds(source.Leds); + Enable(); + + // After loading using the source entity create a new entity so the next call to Save won't mess with the source, just in case. + LayerEntity = new LayerEntity(); } /// @@ -373,7 +385,7 @@ namespace Artemis.Core if (ShouldBeEnabled) Enable(); - else if (Timeline.IsFinished && !Children.Any()) + else if (Timeline.IsFinished && !_renderCopies.Any()) Disable(); if (Timeline.Delta == TimeSpan.Zero) @@ -383,22 +395,26 @@ namespace Artemis.Core 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++) + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { - Layer child = (Layer) Children[index]; + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalUpdate(Timeline); + } + + // Remove render copies that finished their timeline and update the rest + for (int index = 0; index < _renderCopies.Count; index++) + { + Layer child = _renderCopies[index]; if (!child.Timeline.IsFinished) { child.Update(deltaTime); - continue; } - - RemoveChild(child); - child.Dispose(); - index--; + else + { + _renderCopies.Remove(child); + child.Dispose(); + index--; + } } } @@ -408,20 +424,16 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Layer"); - RenderSelf(canvas, basePosition); - RenderChildren(canvas, basePosition); + RenderLayer(canvas, basePosition); + RenderCopies(canvas, basePosition); } - private void RenderSelf(SKCanvas canvas, SKPointI basePosition) + private void RenderLayer(SKCanvas canvas, SKPointI basePosition) { // Ensure the layer is ready 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; @@ -432,7 +444,7 @@ namespace Artemis.Core SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; try { - canvas.Save(); + using SKAutoCanvasRestore _ = new(canvas); canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); using SKPath clipPath = new(Path); clipPath.Transform(SKMatrix.CreateTranslation(Bounds.Left * -1, Bounds.Top * -1)); @@ -478,18 +490,16 @@ namespace Artemis.Core } finally { - canvas.Restore(); layerPaint.DisposeSelfAndProperties(); } Timeline.ClearDelta(); } - private void RenderChildren(SKCanvas canvas, SKPointI basePosition) + private void RenderCopies(SKCanvas canvas, SKPointI basePosition) { - // Render children first so they go below - for (int i = Children.Count - 1; i >= 0; i--) - Children[i].Render(canvas, basePosition); + for (int i = _renderCopies.Count - 1; i >= 0; i--) + _renderCopies[i].Render(canvas, basePosition); } /// @@ -549,23 +559,24 @@ namespace Artemis.Core else Timeline.JumpToEnd(); - while (Children.Any()) + while (_renderCopies.Any()) { - Children[0].Dispose(); - RemoveChild(Children[0]); + _renderCopies[0].Dispose(); + _renderCopies.RemoveAt(0); } } /// - /// Creates a copy of this layer as a child and plays it once + /// Creates a copy of this layer and renders it alongside this layer for as long as its timeline lasts. /// - public void CreateCopyAsChild() + /// The total maximum of render copies to keep + public void CreateRenderCopy(int max) { - Layer copy = new(this, this); - copy.AddLeds(Leds); - copy.Enable(); - copy.Timeline.JumpToStart(); - AddChild(copy); + if (_renderCopies.Count >= max) + return; + + Layer copy = new(this); + _renderCopies.Add(copy); } internal void CalculateRenderProperties() @@ -623,26 +634,23 @@ namespace Artemis.Core 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 + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { - 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); + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); } - finally + using SKAutoCanvasRestore _ = new(canvas); + 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) { - canvas.Restore(); + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); } } diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 4749d4bc3..22167910d 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -46,6 +46,11 @@ public interface IProfileEditorService : IArtemisSharedUIService /// IObservable PixelsPerSecond { get; } + /// + /// Gets an observable of the suspended state. + /// + IObservable SuspendedEditing { get; } + /// /// Gets a source list of all available editor tools. /// @@ -87,6 +92,12 @@ public interface IProfileEditorService : IArtemisSharedUIService /// The new pixels per second. void ChangePixelsPerSecond(int pixelsPerSecond); + /// + /// Changes the current suspended state. + /// + /// The new suspended state. + void ChangeSuspendedEditing(bool suspend); + /// /// Selects the provided keyframe. /// diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 9634b3287..26968f208 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -62,8 +62,6 @@ internal class ProfileEditorService : IProfileEditorService }); } - public IObservable SuspendedEditing { get; } - private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration) { if (profileConfiguration == null) @@ -107,6 +105,7 @@ internal class ProfileEditorService : IProfileEditorService public IObservable ProfileElement { get; } public IObservable LayerProperty { get; } public IObservable History { get; } + public IObservable SuspendedEditing { get; } public IObservable Time { get; } public IObservable Playing { get; } public IObservable PixelsPerSecond { get; } @@ -180,6 +179,25 @@ internal class ProfileEditorService : IProfileEditorService _timeSubject.OnNext(time); } + public void ChangeSuspendedEditing(bool suspend) + { + if (_suspendedEditingSubject.Value == suspend) + return; + + _suspendedEditingSubject.OnNext(suspend); + if (suspend) + { + Pause(); + _profileService.RenderForEditor = false; + } + else + { + if (_profileConfigurationSubject.Value != null) + _profileService.RenderForEditor = true; + Tick(_timeSubject.Value); + } + } + public void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle) { if (keyframe == null) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml index 8a7c99512..b6ead192e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml @@ -5,6 +5,7 @@ xmlns:controls="clr-namespace:Artemis.UI.Controls" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties" xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="350" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView" x:DataType="local:PropertiesViewModel"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs index 75f8b494d..bfd8b37d5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs @@ -31,6 +31,7 @@ public class PropertiesViewModel : ActivatableViewModelBase private ObservableAsPropertyHelper? _layerProperty; private ObservableAsPropertyHelper? _pixelsPerSecond; private ObservableAsPropertyHelper? _profileElement; + private ObservableAsPropertyHelper? _suspendedEditing; /// public PropertiesViewModel(IProfileEditorService profileEditorService, @@ -55,6 +56,7 @@ public class PropertiesViewModel : ActivatableViewModelBase _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); _pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); _layerProperty = profileEditorService.LayerProperty.ToProperty(this, vm => vm.LayerProperty).DisposeWith(d); + _suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d); Disposable.Create(() => { _settingsService.SaveAllSettings(); @@ -94,11 +96,13 @@ public class PropertiesViewModel : ActivatableViewModelBase public RenderProfileElement? ProfileElement => _profileElement?.Value; public Layer? Layer => _profileElement?.Value as Layer; public ILayerProperty? LayerProperty => _layerProperty?.Value; + public bool SuspendedEditing => _suspendedEditing?.Value ?? false; public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0; public IObservable Playing => _profileEditorService.Playing; public PluginSetting PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0); + private void UpdatePropertyGroups() { if (ProfileElement == null) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml index 7aa8d3859..60ef0da43 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml @@ -59,7 +59,7 @@ - + + + +