using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects.Placeholder; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; using SkiaSharp; namespace Artemis.Core; /// /// Represents an element of a that has advanced rendering capabilities /// public abstract class RenderProfileElement : ProfileElement { private SKRectI _bounds; private SKPath? _path; internal RenderProfileElement(ProfileElement parent, Profile profile) : base(profile) { _layerEffects = new List(); _displayCondition = new AlwaysOnCondition(this); Timeline = new Timeline(); LayerEffects = new ReadOnlyCollection(_layerEffects); Parent = parent ?? throw new ArgumentNullException(nameof(parent)); LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; } /// /// Gets a boolean indicating whether this render element and its layers/brushes are enabled /// public bool Enabled { get; protected set; } /// /// Gets a boolean indicating whether this render element and its layers/brushes should be enabled /// public abstract bool ShouldBeEnabled { get; } /// /// Creates a list of all layer properties present on this render element /// /// A list of all layer properties present on this render element public abstract List GetAllLayerProperties(); /// /// Occurs when a layer effect has been added or removed to this render element /// public event EventHandler? LayerEffectsUpdated; /// /// Overrides the main timeline to the specified time and clears any extra time lines /// /// The position to set the timeline to public abstract void OverrideTimelineAndApply(TimeSpan position); /// protected override void Dispose(bool disposing) { LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded; LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved; foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.Dispose(); if (DisplayCondition is IDisposable disposable) disposable.Dispose(); base.Dispose(disposing); } internal void LoadRenderElement() { Timeline = RenderElementEntity.Timeline != null ? new Timeline(RenderElementEntity.Timeline) : new Timeline(); DisplayCondition = RenderElementEntity.DisplayCondition switch { AlwaysOnConditionEntity entity => new AlwaysOnCondition(entity, this), PlayOnceConditionEntity entity => new PlayOnceCondition(entity, this), StaticConditionEntity entity => new StaticCondition(entity, this), EventConditionEntity entity => new EventCondition(entity, this), _ => DisplayCondition }; LoadLayerEffects(); } internal void SaveRenderElement() { RenderElementEntity.LayerEffects.Clear(); foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { baseLayerEffect.Save(); RenderElementEntity.LayerEffects.Add(baseLayerEffect.LayerEffectEntity); } // Condition DisplayCondition?.Save(); RenderElementEntity.DisplayCondition = DisplayCondition?.Entity; // Timeline RenderElementEntity.Timeline = Timeline?.Entity; Timeline?.Save(); } internal void LoadNodeScript() { if (DisplayCondition is INodeScriptCondition scriptCondition) scriptCondition.LoadNodeScript(); foreach (ILayerProperty layerProperty in GetAllLayerProperties()) layerProperty.BaseDataBinding.LoadNodeScript(); } private void OnLayerEffectsUpdated() { LayerEffectsUpdated?.Invoke(this, EventArgs.Empty); } #region Timeline /// /// Gets the timeline associated with this render element /// public Timeline Timeline { get; private set; } /// /// Updates the according to the provided and current display /// condition /// protected void UpdateTimeline(double deltaTime) { DisplayCondition.UpdateTimeline(deltaTime); } #endregion #region Properties internal abstract RenderElementEntity RenderElementEntity { get; } /// /// Gets the parent of this element /// public new ProfileElement Parent { get => base.Parent!; internal set { base.Parent = value; OnPropertyChanged(nameof(Parent)); } } /// /// Gets the path containing all the LEDs this entity is applied to, any rendering outside the entity Path is /// clipped. /// public SKPath? Path { get => _path; protected set { SetAndNotify(ref _path, value); // I can't really be sure about the performance impact of calling Bounds often but // SkiaSharp calls SkiaApi.sk_path_get_bounds (Handle, &rect); which sounds expensive Bounds = SKRectI.Round(value?.Bounds ?? SKRect.Empty); } } /// /// The bounds of this entity /// public SKRectI Bounds { get => _bounds; private set => SetAndNotify(ref _bounds, value); } #endregion #region State /// /// Enables the render element and its brushes and effects /// public abstract void Disable(); /// /// Disables the render element and its brushes and effects /// public abstract void Enable(); #endregion #region Effect management private readonly List _layerEffects; /// /// Gets a read-only collection of the layer effects on this entity /// public ReadOnlyCollection LayerEffects { get; } /// /// Adds a the provided layer effect to the render profile element /// /// The effect to add. public void AddLayerEffect(BaseLayerEffect layerEffect) { if (layerEffect == null) throw new ArgumentNullException(nameof(layerEffect)); // Ensure something needs to be done if (_layerEffects.Contains(layerEffect)) return; // Make sure the layer effect is tied to this element layerEffect.ProfileElement = this; // If the element is enabled, enable the effect before adding it if (Enabled) { layerEffect.InternalEnable(); layerEffect.Update(0); } _layerEffects.Add(layerEffect); // Update the order on the effects OrderEffects(); OnLayerEffectsUpdated(); } /// /// Removes the provided layer effect. /// /// The effect to remove. public void RemoveLayerEffect(BaseLayerEffect layerEffect) { if (layerEffect == null) throw new ArgumentNullException(nameof(layerEffect)); // Ensure something needs to be done if (!_layerEffects.Contains(layerEffect)) return; // Remove the effect from the layer _layerEffects.Remove(layerEffect); // Disable the effect after removing it layerEffect.InternalDisable(); // Update the order on the remaining effects OrderEffects(); OnLayerEffectsUpdated(); } private void LoadLayerEffects() { foreach (BaseLayerEffect baseLayerEffect in _layerEffects) baseLayerEffect.Dispose(); _layerEffects.Clear(); foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects.OrderBy(e => e.Order)) LoadLayerEffect(layerEffectEntity); } private void LoadLayerEffect(LayerEffectEntity layerEffectEntity) { LayerEffectDescriptor? descriptor = LayerEffectStore.Get(layerEffectEntity.ProviderId, layerEffectEntity.EffectType)?.LayerEffectDescriptor; BaseLayerEffect layerEffect; // Create an instance with the descriptor if (descriptor != null) { layerEffect = descriptor.CreateInstance(this, layerEffectEntity); } // If no descriptor was found and there was no existing placeholder, create a placeholder else { descriptor = PlaceholderLayerEffectDescriptor.Create(); layerEffect = descriptor.CreateInstance(this, layerEffectEntity); } _layerEffects.Add(layerEffect); } private void ReplaceLayerEffectWithPlaceholder(BaseLayerEffect layerEffect) { int index = _layerEffects.IndexOf(layerEffect); if (index == -1) return; LayerEffectDescriptor descriptor = PlaceholderLayerEffectDescriptor.Create(); BaseLayerEffect placeholder = descriptor.CreateInstance(this, layerEffect.LayerEffectEntity); _layerEffects[index] = placeholder; layerEffect.Dispose(); OnLayerEffectsUpdated(); } private void ReplacePlaceholderWithLayerEffect(PlaceholderLayerEffect placeholder) { int index = _layerEffects.IndexOf(placeholder); if (index == -1) return; LayerEffectDescriptor? descriptor = LayerEffectStore.Get(placeholder.OriginalEntity.ProviderId, placeholder.OriginalEntity.EffectType)?.LayerEffectDescriptor; if (descriptor == null) throw new ArtemisCoreException("Can't replace a placeholder effect because the real effect isn't available."); BaseLayerEffect layerEffect = descriptor.CreateInstance(this, placeholder.OriginalEntity); _layerEffects[index] = layerEffect; placeholder.Dispose(); OnLayerEffectsUpdated(); } private void OrderEffects() { int index = 0; foreach (BaseLayerEffect baseLayerEffect in LayerEffects.OrderBy(e => e.Order)) { baseLayerEffect.Order = index + 1; index++; } _layerEffects.Sort((a, b) => a.Order.CompareTo(b.Order)); } private void LayerEffectStoreOnLayerEffectRemoved(object? sender, LayerEffectStoreEvent e) { // Find effects that just got disabled and replace them with placeholders List affectedLayerEffects = _layerEffects.Where(e.Registration.Matches).ToList(); if (!affectedLayerEffects.Any()) return; foreach (BaseLayerEffect baseLayerEffect in affectedLayerEffects) ReplaceLayerEffectWithPlaceholder(baseLayerEffect); OnLayerEffectsUpdated(); } private void LayerEffectStoreOnLayerEffectAdded(object? sender, LayerEffectStoreEvent e) { // Find placeholders that just got enabled and replace them with real effects List affectedPlaceholders = LayerEffects .Where(l => l is PlaceholderLayerEffect ph && e.Registration.Matches(ph)) .Cast() .ToList(); if (!affectedPlaceholders.Any()) return; foreach (PlaceholderLayerEffect placeholderLayerEffect in affectedPlaceholders) ReplacePlaceholderWithLayerEffect(placeholderLayerEffect); OnLayerEffectsUpdated(); } #endregion #region Conditions /// /// Gets whether the display conditions applied to this layer where met or not during last update /// Always true if the layer has no display conditions /// public bool DisplayConditionMet { get => _displayConditionMet; private set => SetAndNotify(ref _displayConditionMet, value); } private bool _displayConditionMet; /// /// Gets or sets the display condition used to determine whether this element is active or not /// public ICondition DisplayCondition { get => _displayCondition; set => SetAndNotify(ref _displayCondition, value); } private ICondition _displayCondition; /// /// Evaluates the display conditions on this element and applies any required changes to the /// public void UpdateDisplayCondition() { if (Suspended) { DisplayConditionMet = false; return; } DisplayCondition.Update(); DisplayConditionMet = DisplayCondition.IsMet; } #endregion }