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
}
}