using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; 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; Enabled = true; _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); _leds = new List(); 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(); 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; } 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}"; } #region IDisposable /// protected override void Dispose(bool disposing) { Disposed = true; // Brush first in case it depends on any of the other disposables during it's own disposal _layerBrush?.Dispose(); _general.Dispose(); _transform.Dispose(); Renderer.Dispose(); base.Dispose(disposing); } #endregion 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(); } #region Storage internal override void Load() { EntityId = LayerEntity.Id; Name = LayerEntity.Name; Enabled = LayerEntity.Enabled; Order = LayerEntity.Order; ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); LoadRenderElement(); } internal override void Save() { if (Disposed) throw new ObjectDisposedException("Layer"); // Properties LayerEntity.Id = EntityId; LayerEntity.ParentId = Parent?.EntityId ?? new Guid(); LayerEntity.Order = Order; LayerEntity.Enabled = Enabled; 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.RgbDevice.GetDeviceIdentifier(), LedName = artemisLed.RgbLed.Id.ToString() }; LayerEntity.Leds.Add(ledEntity); } 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"); if (!Enabled) return; UpdateDisplayCondition(); UpdateTimeline(deltaTime); } /// public override void Reset() { DisplayConditionMet = false; Timeline.JumpToEnd(); } /// public override void Render(SKCanvas canvas) { 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?.BaseProperties?.PropertiesInitialized == false || LayerBrush?.BrushType != LayerBrushType.Regular) return; RenderTimeline(Timeline, canvas); foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList()) RenderTimeline(extraTimeline, canvas); Timeline.ClearDelta(); } private void ApplyTimeline(Timeline timeline) { General.Update(timeline); Transform.Update(timeline); if (LayerBrush != null) { LayerBrush.BaseProperties?.Update(timeline); LayerBrush.Update(timeline.Delta.TotalSeconds); } foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) { baseLayerEffect.BaseProperties?.Update(timeline); baseLayerEffect.Update(timeline.Delta.TotalSeconds); } } private void RenderTimeline(Timeline timeline, SKCanvas canvas) { if (Path == null || LayerBrush == null) throw new ArtemisCoreException("The layer is not yet ready for rendering"); if (timeline.IsFinished) return; ApplyTimeline(timeline); try { canvas.Save(); Renderer.Open(Path, Parent as Folder); if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null) throw new ArtemisCoreException("Failed to open layer render context"); // Apply blend mode and color Renderer.Paint.BlendMode = General.BlendMode.CurrentValue; Renderer.Paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); using SKPath renderPath = new(); if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle) renderPath.AddRect(Renderer.Path.Bounds); else renderPath.AddOval(Renderer.Path.Bounds); 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); Renderer.Canvas.SetMatrix(Renderer.Canvas.TotalMatrix.PreConcat(rotationMatrix)); } // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off Renderer.Canvas.ClipPath(renderPath); DelegateRendering(renderPath.Bounds); } else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip) { SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true); renderPath.Transform(renderPathMatrix); // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off Renderer.Canvas.ClipPath(renderPath); DelegateRendering(Renderer.Path.Bounds); } canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint); } finally { try { canvas.Restore(); } catch { // ignored } Renderer.Close(); } } private void DelegateRendering(SKRect bounds) { if (LayerBrush == null) throw new ArtemisCoreException("The layer is not yet ready for rendering"); if (Renderer.Canvas == null || Renderer.Paint == null) throw new ArtemisCoreException("Failed to open layer render context"); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) baseLayerEffect.PreProcess(Renderer.Canvas, bounds, Renderer.Paint); LayerBrush.InternalRender(Renderer.Canvas, bounds, Renderer.Paint); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) baseLayerEffect.PostProcess(Renderer.Canvas, bounds, Renderer.Paint); } 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(SKPath layerPath, 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 SKPoint(layerPath.Bounds.MidX - layerPath.Bounds.Left, layerPath.Bounds.MidY - layerPath.Bounds.Top) : new SKPoint(layerPath.Bounds.MidX, layerPath.Bounds.MidY); // Apply translation if (applyTranslation) { position.X += positionProperty.X * layerPath.Bounds.Width; position.Y += positionProperty.Y * layerPath.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(Path, 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); 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(ArtemisSurface surface) { if (Disposed) throw new ObjectDisposedException("Layer"); List leds = new(); // Get the surface LEDs for this layer List availableLeds = surface.Devices.Where(d => d.IsEnabled).SelectMany(d => d.Leds).ToList(); foreach (LedEntity ledEntity in LayerEntity.Leds) { ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.RgbDevice.GetDeviceIdentifier() == 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() { 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(); } internal void DeactivateLayerBrush() { if (LayerBrush == null) return; BaseLayerBrush brush = LayerBrush; LayerBrush = null; brush.Dispose(); OnLayerBrushUpdated(); } #endregion #region Event handlers 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(); } #endregion #region Events /// /// 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; private void OnRenderPropertiesUpdated() { RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); } internal void OnLayerBrushUpdated() { LayerBrushUpdated?.Invoke(this, EventArgs.Empty); } #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 } }