using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; using RGB.NET.Core; using SkiaSharp; namespace Artemis.Core { /// /// Represents a layer in a /// public sealed class Layer : RenderProfileElement { private LayerGeneralProperties _general; private BaseLayerBrush? _layerBrush; private LayerShape? _layerShape; private List _leds; private LayerTransformProperties _transform; /// /// Creates a new instance of the class and adds itself to the child collection of the provided /// /// /// The parent of the layer /// The name of the layer public Layer(ProfileElement parent, string name) : base(parent.Profile) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Profile = Parent.Profile; Name = name; Suspended = false; _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); _leds = new List(); Adapter = new LayerAdapter(this); Initialize(); Parent.AddChild(this, 0); } /// /// Creates a new instance of the class based on the provided layer entity /// /// The profile the layer belongs to /// The parent of the layer /// The entity of the layer public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent.Profile) { LayerEntity = layerEntity; EntityId = layerEntity.Id; Profile = profile; Parent = parent; _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); _leds = new List(); Adapter = new LayerAdapter(this); Load(); Initialize(); } /// /// A collection of all the LEDs this layer is assigned to. /// public ReadOnlyCollection Leds => _leds.AsReadOnly(); /// /// Defines the shape that is rendered by the . /// public LayerShape? LayerShape { get => _layerShape; set { SetAndNotify(ref _layerShape, value); if (Path != null) CalculateRenderProperties(); } } /// /// Gets the general properties of the layer /// [PropertyGroupDescription(Name = "General", Description = "A collection of general properties")] public LayerGeneralProperties General { get => _general; private set => SetAndNotify(ref _general, value); } /// /// Gets the transform properties of the layer /// [PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")] public LayerTransformProperties Transform { get => _transform; private set => SetAndNotify(ref _transform, value); } /// /// The brush that will fill the . /// public BaseLayerBrush? LayerBrush { get => _layerBrush; internal set => SetAndNotify(ref _layerBrush, value); } /// /// Gets the layer entity this layer uses for persistent storage /// public LayerEntity LayerEntity { get; internal set; } /// /// Gets the layer adapter that can be used to adapt this layer to a different set of devices /// public LayerAdapter Adapter { get; } /// public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; internal override RenderElementEntity RenderElementEntity => LayerEntity; /// public override List GetAllLayerProperties() { List result = new(); result.AddRange(General.GetAllLayerProperties()); result.AddRange(Transform.GetAllLayerProperties()); if (LayerBrush?.BaseProperties != null) result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); foreach (BaseLayerEffect layerEffect in LayerEffects) { if (layerEffect.BaseProperties != null) result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); } return result; } /// public override string ToString() { return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } /// /// Occurs when a property affecting the rendering properties of this layer has been updated /// public event EventHandler? RenderPropertiesUpdated; /// /// Occurs when the layer brush of this layer has been updated /// public event EventHandler? LayerBrushUpdated; /// protected override void Dispose(bool disposing) { Disable(); Disposed = true; LayerBrushStore.LayerBrushAdded -= LayerBrushStoreOnLayerBrushAdded; LayerBrushStore.LayerBrushRemoved -= LayerBrushStoreOnLayerBrushRemoved; // Brush first in case it depends on any of the other disposables during it's own disposal _layerBrush?.Dispose(); _general.Dispose(); _transform.Dispose(); base.Dispose(disposing); } internal void OnLayerBrushUpdated() { LayerBrushUpdated?.Invoke(this, EventArgs.Empty); } private void Initialize() { LayerBrushStore.LayerBrushAdded += LayerBrushStoreOnLayerBrushAdded; LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; // Layers have two hardcoded property groups, instantiate them Attribute generalAttribute = Attribute.GetCustomAttribute( GetType().GetProperty(nameof(General))!, typeof(PropertyGroupDescriptionAttribute) )!; Attribute transformAttribute = Attribute.GetCustomAttribute( GetType().GetProperty(nameof(Transform))!, typeof(PropertyGroupDescriptionAttribute) )!; General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; General.Initialize(this, "General.", Constants.CorePluginFeature); Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute; Transform.Initialize(this, "Transform.", Constants.CorePluginFeature); General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; ApplyShapeType(); ActivateLayerBrush(); Reset(); } private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) { if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) DeactivateLayerBrush(); } private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) { if (LayerBrush != null || !General.PropertiesInitialized) return; LayerBrushReference? current = General.BrushReference.CurrentValue; if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) ActivateLayerBrush(); } private void OnRenderPropertiesUpdated() { RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); } #region Storage internal override void Load() { EntityId = LayerEntity.Id; Name = LayerEntity.Name; Suspended = LayerEntity.Suspended; Order = LayerEntity.Order; ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); LoadRenderElement(); Adapter.Load(); } internal override void Save() { if (Disposed) throw new ObjectDisposedException("Layer"); // Properties LayerEntity.Id = EntityId; LayerEntity.ParentId = Parent?.EntityId ?? new Guid(); LayerEntity.Order = Order; LayerEntity.Suspended = Suspended; LayerEntity.Name = Name; LayerEntity.ProfileId = Profile.EntityId; LayerEntity.ExpandedPropertyGroups.Clear(); LayerEntity.ExpandedPropertyGroups.AddRange(ExpandedPropertyGroups); General.ApplyToEntity(); Transform.ApplyToEntity(); LayerBrush?.BaseProperties?.ApplyToEntity(); // LEDs LayerEntity.Leds.Clear(); foreach (ArtemisLed artemisLed in Leds) { LedEntity ledEntity = new() { DeviceIdentifier = artemisLed.Device.Identifier, LedName = artemisLed.RgbLed.Id.ToString(), PhysicalLayout = artemisLed.Device.DeviceType == RGBDeviceType.Keyboard ? (int) artemisLed.Device.PhysicalLayout : null }; LayerEntity.Leds.Add(ledEntity); } // Adaption hints Adapter.Save(); SaveRenderElement(); } #endregion #region Shape management private void ShapeTypeOnCurrentValueSet(object? sender, EventArgs e) { ApplyShapeType(); } private void ApplyShapeType() { LayerShape = General.ShapeType.CurrentValue switch { LayerShapeType.Ellipse => new EllipseShape(this), LayerShapeType.Rectangle => new RectangleShape(this), _ => throw new ArgumentOutOfRangeException() }; } #endregion #region Rendering /// public override void Update(double deltaTime) { if (Disposed) throw new ObjectDisposedException("Layer"); UpdateDisplayCondition(); UpdateTimeline(deltaTime); if (ShouldBeEnabled) Enable(); else if (Timeline.IsFinished) Disable(); } /// public override void Reset() { UpdateDisplayCondition(); if (DisplayConditionMet) Timeline.JumpToStart(); else Timeline.JumpToEnd(); } /// public override void Render(SKCanvas canvas, SKPointI basePosition) { if (Disposed) throw new ObjectDisposedException("Layer"); // Ensure the layer is ready if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized) return; // 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 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) return; SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; try { canvas.Save(); 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)); canvas.ClipPath(clipPath, SKClipOperation.Intersect, true); SKRectI layerBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); // Apply blend mode and color layerPaint.BlendMode = General.BlendMode.CurrentValue; layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); using SKPath renderPath = new(); if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle) renderPath.AddRect(layerBounds); else renderPath.AddOval(layerBounds); if (General.TransformMode.CurrentValue == LayerTransformMode.Normal) { // Apply transformation except rotation to the render path if (LayerBrush.SupportsTransformation) { SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, false); renderPath.Transform(renderPathMatrix); } // Apply rotation to the canvas if (LayerBrush.SupportsTransformation) { SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true); canvas.SetMatrix(canvas.TotalMatrix.PreConcat(rotationMatrix)); } DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint); } else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip) { SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true); renderPath.Transform(renderPathMatrix); DelegateRendering(canvas, renderPath, layerBounds, layerPaint); } } finally { canvas.Restore(); layerPaint.DisposeSelfAndProperties(); } } 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(); } } internal void CalculateRenderProperties() { if (Disposed) throw new ObjectDisposedException("Layer"); if (!Leds.Any()) Path = new SKPath(); else { SKPath path = new() {FillType = SKPathFillType.Winding}; foreach (ArtemisLed artemisLed in Leds) path.AddRect(artemisLed.AbsoluteRectangle); Path = path; } // This is called here so that the shape's render properties are up to date when other code // responds to OnRenderPropertiesUpdated LayerShape?.CalculateRenderProperties(); // Folder render properties are based on child paths and thus require an update if (Parent is Folder folder) folder.CalculateRenderProperties(); OnRenderPropertiesUpdated(); } internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased) { if (Disposed) throw new ObjectDisposedException("Layer"); SKPoint positionProperty = Transform.Position.CurrentValue; // Start at the center of the shape SKPoint position = zeroBased ? new SKPointI(Bounds.MidX - Bounds.Left, Bounds.MidY - Bounds.Top) : new SKPointI(Bounds.MidX, Bounds.MidY); // Apply translation if (applyTranslation) { position.X += positionProperty.X * Bounds.Width; position.Y += positionProperty.Y * Bounds.Height; } return position; } /// /// Creates a transformation matrix that applies the current transformation settings /// /// /// If true, treats the layer as if it is located at 0,0 instead of its actual position on the /// surface /// /// Whether translation should be included /// Whether the scale should be included /// Whether the rotation should be included /// The transformation matrix containing the current transformation settings public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation) { if (Disposed) throw new ObjectDisposedException("Layer"); if (Path == null) return SKMatrix.Empty; SKSize sizeProperty = Transform.Scale.CurrentValue; float rotationProperty = Transform.Rotation.CurrentValue; SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased); SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor float x = anchorPosition.X - (zeroBased ? Bounds.MidX - Bounds.Left : Bounds.MidX) - anchorProperty.X * Bounds.Width; float y = anchorPosition.Y - (zeroBased ? Bounds.MidY - Bounds.Top : Bounds.MidY) - anchorProperty.Y * Bounds.Height; SKMatrix transform = SKMatrix.Empty; if (includeTranslation) // transform is always SKMatrix.Empty here... transform = SKMatrix.CreateTranslation(x, y); if (includeScale) { if (transform == SKMatrix.Empty) transform = SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); else transform = transform.PostConcat(SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y)); } if (includeRotation) { if (transform == SKMatrix.Empty) transform = SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y); else transform = transform.PostConcat(SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y)); } return transform; } #endregion #region LED management /// /// Adds a new to the layer and updates the render properties. /// /// The LED to add public void AddLed(ArtemisLed led) { if (Disposed) throw new ObjectDisposedException("Layer"); _leds.Add(led); CalculateRenderProperties(); } /// /// Adds a collection of new s to the layer and updates the render properties. /// /// The LEDs to add public void AddLeds(IEnumerable leds) { if (Disposed) throw new ObjectDisposedException("Layer"); _leds.AddRange(leds.Except(_leds)); CalculateRenderProperties(); } /// /// Removes a from the layer and updates the render properties. /// /// The LED to remove public void RemoveLed(ArtemisLed led) { if (Disposed) throw new ObjectDisposedException("Layer"); _leds.Remove(led); CalculateRenderProperties(); } /// /// Removes all s from the layer and updates the render properties. /// public void ClearLeds() { if (Disposed) throw new ObjectDisposedException("Layer"); _leds.Clear(); CalculateRenderProperties(); } internal void PopulateLeds(IEnumerable devices) { if (Disposed) throw new ObjectDisposedException("Layer"); List leds = new(); // Get the surface LEDs for this layer List availableLeds = devices.SelectMany(d => d.Leds).ToList(); foreach (LedEntity ledEntity in LayerEntity.Leds) { ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier && a.RgbLed.Id.ToString() == ledEntity.LedName); if (match != null) leds.Add(match); } _leds = leds; CalculateRenderProperties(); } #endregion #region Brush management /// /// Changes the current layer brush to the brush described in the provided /// public void ChangeLayerBrush(LayerBrushDescriptor descriptor) { if (descriptor == null) throw new ArgumentNullException(nameof(descriptor)); if (LayerBrush != null) { BaseLayerBrush brush = LayerBrush; LayerBrush = null; brush.Dispose(); } // Ensure the brush reference matches the brush LayerBrushReference? current = General.BrushReference.BaseValue; if (!descriptor.MatchesLayerBrushReference(current)) General.BrushReference.BaseValue = new LayerBrushReference(descriptor); ActivateLayerBrush(); } /// /// Removes the current layer brush from the layer /// public void RemoveLayerBrush() { if (LayerBrush == null) return; BaseLayerBrush brush = LayerBrush; DeactivateLayerBrush(); LayerEntity.PropertyEntities.RemoveAll(p => p.FeatureId == brush.ProviderId && p.Path.StartsWith("LayerBrush.")); } internal void ActivateLayerBrush() { try { LayerBrushReference? current = General.BrushReference.CurrentValue; if (current == null) return; LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null ? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor : null; descriptor?.CreateInstance(this); OnLayerBrushUpdated(); ClearBrokenState("Failed to initialize layer brush"); } catch (Exception e) { SetBrokenState("Failed to initialize layer brush", e); } } internal void DeactivateLayerBrush() { if (LayerBrush == null) return; BaseLayerBrush brush = LayerBrush; LayerBrush = null; brush.Dispose(); OnLayerBrushUpdated(); } #endregion #region Overrides of BreakableModel /// public override IEnumerable GetBrokenHierarchy() { if (LayerBrush?.BrokenState != null) yield return LayerBrush; foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null)) yield return baseLayerEffect; } #endregion } /// /// Represents a type of layer shape /// public enum LayerShapeType { /// /// A circular layer shape /// Ellipse, /// /// A rectangular layer shape /// Rectangle } /// /// Represents a layer transform mode /// public enum LayerTransformMode { /// /// Normal transformation /// Normal, /// /// Transforms only a clip /// Clip } }