1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-31 17:53:32 +00:00

Profiles - Removed the technical concept of extra timelines for events

Profiles - Properly separated out update and render logic
Profile editor - Implemented layer visualization
This commit is contained in:
Robert 2022-02-04 00:15:00 +01:00
parent ff4ec16690
commit 89beb92935
18 changed files with 431 additions and 292 deletions

View File

@ -158,8 +158,8 @@ namespace Artemis.Core
// If the timeline was already running, look at the event overlap mode // If the timeline was already running, look at the event overlap mode
if (EventOverlapMode == TimeLineEventOverlapMode.Restart) if (EventOverlapMode == TimeLineEventOverlapMode.Restart)
timeline.JumpToStart(); timeline.JumpToStart();
else if (EventOverlapMode == TimeLineEventOverlapMode.Copy) else if (EventOverlapMode == TimeLineEventOverlapMode.Copy && ProfileElement is Layer layer)
timeline.AddExtraTimeline(); layer.CreateCopyAsChild();
else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet) else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet)
timeline.JumpToStart(); timeline.JumpToStart();

View File

@ -102,6 +102,9 @@ namespace Artemis.Core
else if (Timeline.IsFinished) else if (Timeline.IsFinished)
Disable(); Disable();
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
foreach (ProfileElement child in Children) foreach (ProfileElement child in Children)
child.Update(deltaTime); child.Update(deltaTime);
} }
@ -176,41 +179,35 @@ namespace Artemis.Core
// No point rendering if all children are disabled // No point rendering if all children are disabled
if (!Children.Any(c => c is RenderProfileElement {Enabled: true})) if (!Children.Any(c => c is RenderProfileElement {Enabled: true}))
return; return;
lock (Timeline) SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
try
{ {
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);
baseLayerEffect.InternalUpdate(Timeline); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint);
SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; // No point rendering if the alpha was set to zero by one of the effects
try if (layerPaint.Color.Alpha == 0)
{ return;
SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint);
// No point rendering if the alpha was set to zero by one of the effects canvas.SaveLayer(layerPaint);
if (layerPaint.Color.Alpha == 0) canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
return;
canvas.SaveLayer(layerPaint); // Iterate the children in reverse because the first layer must be rendered last to end up on top
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); for (int index = Children.Count - 1; index > -1; index--)
Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top));
// Iterate the children in reverse because the first layer must be rendered last to end up on top foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
for (int index = Children.Count - 1; index > -1; index--) baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint);
Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top));
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint);
}
finally
{
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
Timeline.ClearDelta();
} }
finally
{
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
Timeline.ClearDelta();
} }
#endregion #endregion
@ -233,6 +230,14 @@ namespace Artemis.Core
Enabled = true; Enabled = true;
} }
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment)
{
Timeline.Override(position, stickToMainSegment);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
}
/// <inheritdoc /> /// <inheritdoc />
public override void Disable() public override void Disable()
{ {
@ -314,7 +319,7 @@ namespace Artemis.Core
ChildrenList.Add(new Layer(Profile, this, childLayer)); ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me // Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList.Sort((a,b) => a.Order.CompareTo(b.Order)); ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order));
for (int index = 0; index < ChildrenList.Count; index++) for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1; ChildrenList[index].Order = index + 1;

View File

@ -345,17 +345,30 @@ namespace Artemis.Core
Enable(); Enable();
else if (Timeline.IsFinished) else if (Timeline.IsFinished)
Disable(); Disable();
}
/// <inheritdoc /> if (Timeline.Delta == TimeSpan.Zero)
public override void Reset() return;
{
UpdateDisplayCondition();
if (DisplayConditionMet) General.Update(Timeline);
Timeline.JumpToStart(); Transform.Update(Timeline);
else LayerBrush?.InternalUpdate(Timeline);
Timeline.JumpToEnd();
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++)
{
ProfileElement profileElement = Children[index];
if (((Layer) profileElement).Timeline.IsFinished)
{
RemoveChild(profileElement);
profileElement.Dispose();
index--;
}
else
profileElement.Update(deltaTime);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -365,88 +378,18 @@ namespace Artemis.Core
throw new ObjectDisposedException("Layer"); throw new ObjectDisposedException("Layer");
// Ensure the layer is ready // Ensure the layer is ready
if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized) if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized || !Leds.Any())
return; 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 // Ensure the brush is ready
if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false) if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false)
return; return;
RenderTimeline(Timeline, canvas, basePosition); if (Timeline.IsFinished || LayerBrush?.BrushType != LayerBrushType.Regular)
foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList())
RenderTimeline(extraTimeline, canvas, basePosition);
Timeline.ClearDelta();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override void Activate()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Deactivate()
{
throw new NotImplementedException();
}
/// <inheritdoc />
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; return;
SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
@ -501,34 +444,96 @@ namespace Artemis.Core
canvas.Restore(); canvas.Restore();
layerPaint.DisposeSelfAndProperties(); layerPaint.DisposeSelfAndProperties();
} }
Timeline.ClearDelta();
} }
private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) /// <inheritdoc />
public override void Enable()
{ {
if (LayerBrush == null) if (Enabled)
throw new ArtemisCoreException("The layer is not yet ready for rendering"); 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;
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment)
{
Timeline.Override(position, stickToMainSegment);
General.Update(Timeline);
Transform.Update(Timeline);
LayerBrush?.InternalUpdate(Timeline);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); baseLayerEffect.InternalUpdate(Timeline);
}
try /// <inheritdoc />
public override void Activate()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Deactivate()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Disable()
{
if (!Enabled)
return;
LayerBrush?.InternalDisable();
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
Enabled = false;
}
/// <inheritdoc />
public override void Reset()
{
UpdateDisplayCondition();
if (DisplayConditionMet)
Timeline.JumpToStart();
else
Timeline.JumpToEnd();
while (Children.Any())
{ {
canvas.SaveLayer(layerPaint); Children[0].Dispose();
canvas.ClipPath(renderPath); RemoveChild(Children[0]);
// 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 /// <summary>
{ /// Creates a copy of this layer as a child and plays it once
canvas.Restore(); /// </summary>
} public void CreateCopyAsChild()
{
throw new NotImplementedException();
// Create a copy of the layer and it's properties
// Add to children
} }
internal void CalculateRenderProperties() internal void CalculateRenderProperties()
@ -582,6 +587,34 @@ namespace Artemis.Core
return position; return position;
} }
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();
}
}
/// <summary> /// <summary>
/// Creates a transformation matrix that applies the current transformation settings /// Creates a transformation matrix that applies the current transformation settings
/// </summary> /// </summary>

View File

@ -49,6 +49,12 @@ namespace Artemis.Core
Keyframes = new ReadOnlyCollection<LayerPropertyKeyframe<T>>(_keyframes); Keyframes = new ReadOnlyCollection<LayerPropertyKeyframe<T>>(_keyframes);
} }
/// <inheritdoc />
public override string ToString()
{
return $"{Path} - {CurrentValue} ({PropertyType})";
}
/// <summary> /// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary> /// </summary>

View File

@ -137,7 +137,7 @@ namespace Artemis.Core
/// Updates the <see cref="Timeline" /> according to the provided <paramref name="deltaTime" /> and current display /// Updates the <see cref="Timeline" /> according to the provided <paramref name="deltaTime" /> and current display
/// condition status /// condition status
/// </summary> /// </summary>
public void UpdateTimeline(double deltaTime) protected void UpdateTimeline(double deltaTime)
{ {
// TODO: Move to conditions // TODO: Move to conditions
@ -327,7 +327,7 @@ namespace Artemis.Core
OrderEffects(); OrderEffects();
} }
internal void ActivateLayerEffect(BaseLayerEffect layerEffect) internal void ActivateLayerEffect(BaseLayerEffect layerEffect)
{ {
@ -406,5 +406,12 @@ namespace Artemis.Core
} }
#endregion #endregion
/// <summary>
/// Overrides the main timeline to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public abstract void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment);
} }
} }

View File

@ -11,7 +11,6 @@ namespace Artemis.Core;
/// </summary> /// </summary>
public class Timeline : CorePropertyChanged, IStorageModel public class Timeline : CorePropertyChanged, IStorageModel
{ {
private const int MaxExtraTimelines = 15;
private readonly object _lock = new(); private readonly object _lock = new();
/// <summary> /// <summary>
@ -21,78 +20,33 @@ public class Timeline : CorePropertyChanged, IStorageModel
{ {
Entity = new TimelineEntity(); Entity = new TimelineEntity();
MainSegmentLength = TimeSpan.FromSeconds(5); MainSegmentLength = TimeSpan.FromSeconds(5);
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
Save(); Save();
} }
internal Timeline(TimelineEntity entity) internal Timeline(TimelineEntity entity)
{ {
Entity = entity; Entity = entity;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
Load(); Load();
} }
private Timeline(Timeline parent)
{
Entity = new TimelineEntity();
Parent = parent;
StartSegmentLength = Parent.StartSegmentLength;
MainSegmentLength = Parent.MainSegmentLength;
EndSegmentLength = Parent.EndSegmentLength;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
}
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return $"Progress: {Position}/{Length} - delta: {Delta}"; return $"Progress: {Position}/{Length} - delta: {Delta}";
} }
#region Extra timelines
/// <summary>
/// Adds an extra timeline to this timeline
/// </summary>
public void AddExtraTimeline()
{
_extraTimelines.Add(new Timeline(this));
if (_extraTimelines.Count > MaxExtraTimelines)
_extraTimelines.RemoveAt(0);
}
/// <summary>
/// Removes all extra timelines from this timeline
/// </summary>
public void ClearExtraTimelines()
{
_extraTimelines.Clear();
}
#endregion
#region Properties #region Properties
private TimeSpan _position; private TimeSpan _position;
private TimeSpan _lastDelta; private TimeSpan _lastDelta;
private TimelinePlayMode _playMode; private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode; private TimelineStopMode _stopMode;
private readonly List<Timeline> _extraTimelines;
private TimeSpan _startSegmentLength; private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength; private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength; private TimeSpan _endSegmentLength;
/// <summary>
/// Gets the parent this timeline is an extra timeline of
/// </summary>
public Timeline? Parent { get; }
/// <summary> /// <summary>
/// Gets the current position of the timeline /// Gets the current position of the timeline
/// </summary> /// </summary>
@ -105,21 +59,13 @@ public class Timeline : CorePropertyChanged, IStorageModel
/// <summary> /// <summary>
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to /// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
/// <see cref="ClearDelta" /> /// <see cref="ClearDelta" />
/// <para>
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
/// </para>
/// </summary> /// </summary>
public TimeSpan Delta public TimeSpan Delta
{ {
get => Parent == null ? _lastDelta : DeltaToParent; get => _lastDelta;
private set => SetAndNotify(ref _lastDelta, value); private set => SetAndNotify(ref _lastDelta, value);
} }
/// <summary>
/// Gets the delta to this timeline's <see cref="Parent" />
/// </summary>
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
/// <summary> /// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met /// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary> /// </summary>
@ -138,15 +84,10 @@ public class Timeline : CorePropertyChanged, IStorageModel
set => SetAndNotify(ref _stopMode, value); set => SetAndNotify(ref _stopMode, value);
} }
/// <summary>
/// Gets a list of extra copies of the timeline applied to this timeline
/// </summary>
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
/// <summary> /// <summary>
/// Gets a boolean indicating whether the timeline has finished its run /// Gets a boolean indicating whether the timeline has finished its run
/// </summary> /// </summary>
public bool IsFinished => Position > Length && !ExtraTimelines.Any(); public bool IsFinished => Position > Length;
/// <summary> /// <summary>
/// Gets a boolean indicating whether the timeline progress has been overridden /// Gets a boolean indicating whether the timeline progress has been overridden
@ -327,19 +268,15 @@ public class Timeline : CorePropertyChanged, IStorageModel
IsOverridden = false; IsOverridden = false;
_lastOverridePosition = Position; _lastOverridePosition = Position;
if (stickToMainSegment && Position > MainSegmentEndPosition) if (!stickToMainSegment || Position <= MainSegmentEndPosition)
{ return;
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
_extraTimelines.RemoveAll(t => t.IsFinished); // If the main segment has no length, simply stick to the start of the segment
foreach (Timeline extraTimeline in _extraTimelines) if (MainSegmentLength == TimeSpan.Zero)
extraTimeline.Update(delta, false); Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
} }
} }
@ -389,11 +326,11 @@ public class Timeline : CorePropertyChanged, IStorageModel
} }
/// <summary> /// <summary>
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines /// Overrides the <see cref="Position" /> to the specified time
/// </summary> /// </summary>
/// <param name="position">The position to set the timeline to</param> /// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param> /// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Override(TimeSpan position, bool stickToMainSegment) internal void Override(TimeSpan position, bool stickToMainSegment)
{ {
lock (_lock) lock (_lock)
{ {
@ -403,24 +340,22 @@ public class Timeline : CorePropertyChanged, IStorageModel
IsOverridden = true; IsOverridden = true;
_lastOverridePosition = position; _lastOverridePosition = position;
if (stickToMainSegment && Position >= MainSegmentStartPosition) if (!stickToMainSegment || Position < MainSegmentStartPosition)
{ return;
bool atSegmentStart = Position == MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
{
Position = MainSegmentStartPosition;
}
}
_extraTimelines.Clear(); bool atSegmentStart = Position == MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
{
Position = MainSegmentStartPosition;
}
} }
} }

View File

@ -146,10 +146,11 @@ namespace Artemis.UI.Shared.Services
else else
{ {
renderElement.Enable(); renderElement.Enable();
renderElement.Timeline.Override( renderElement.OverrideTimelineAndApply(
CurrentTime, CurrentTime,
(renderElement != SelectedProfileElement || renderElement.Timeline.Length < CurrentTime) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat (renderElement != SelectedProfileElement || renderElement.Timeline.Length < CurrentTime) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat
); );
renderElement.Update(0);
foreach (ProfileElement child in renderElement.Children) foreach (ProfileElement child in renderElement.Children)
TickProfileElement(child); TickProfileElement(child);

View File

@ -79,10 +79,8 @@ internal class ProfileEditorService : IProfileEditorService
else else
{ {
renderElement.Enable(); renderElement.Enable();
renderElement.Timeline.Override( bool stickToMainSegment = (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat;
time, renderElement.OverrideTimelineAndApply(time, stickToMainSegment);
(renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat
);
foreach (ProfileElement child in renderElement.Children) foreach (ProfileElement child in renderElement.Children)
TickProfileElement(child, time); TickProfileElement(child, time);

View File

@ -56,6 +56,9 @@
<Compile Update="Screens\ProfileEditor\Panels\Properties\PropertiesView.axaml.cs"> <Compile Update="Screens\ProfileEditor\Panels\Properties\PropertiesView.axaml.cs">
<DependentUpon>PropertiesView.axaml</DependentUpon> <DependentUpon>PropertiesView.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Screens\ProfileEditor\Panels\VisualEditor\Visualizers\LayerShapeVisualizerView.axaml.cs">
<DependentUpon>LayerShapeVisualizerView.axaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="DefaultTypes\DataModel\Display\" /> <Folder Include="DefaultTypes\DataModel\Display\" />

View File

@ -65,6 +65,7 @@ namespace Artemis.UI.Ninject.Factories
ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen);
FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder); FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder);
LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer); LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer);
LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer);
LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer); LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer);
} }

View File

@ -48,6 +48,12 @@
<!-- The middle layer contains visualizers --> <!-- The middle layer contains visualizers -->
<ItemsControl Items="{CompiledBinding Visualizers}" ClipToBounds="False"> <ItemsControl Items="{CompiledBinding Visualizers}" ClipToBounds="False">
<ItemsControl.Styles>
<Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<Canvas /> <Canvas />

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Artemis.Core; using Artemis.Core;
@ -8,6 +9,8 @@ using Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor;
@ -16,14 +19,22 @@ public class VisualEditorViewModel : ActivatableViewModelBase
{ {
private readonly IProfileEditorVmFactory _vmFactory; private readonly IProfileEditorVmFactory _vmFactory;
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration; private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
private readonly SourceList<IVisualizerViewModel> _visualizers;
public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory) public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory)
{ {
_vmFactory = vmFactory; _vmFactory = vmFactory;
_visualizers = new SourceList<IVisualizerViewModel>();
Devices = new ObservableCollection<ArtemisDevice>(rgbService.EnabledDevices); Devices = new ObservableCollection<ArtemisDevice>(rgbService.EnabledDevices);
Visualizers = new ObservableCollection<IVisualizerViewModel>();
Tools = new ObservableCollection<IToolViewModel>(); Tools = new ObservableCollection<IToolViewModel>();
_visualizers.Connect()
.Sort(SortExpressionComparer<IVisualizerViewModel>.Ascending(vm => vm.Order))
.Bind(out ReadOnlyObservableCollection<IVisualizerViewModel> visualizers)
.Subscribe();
Visualizers = visualizers;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
@ -34,21 +45,24 @@ public class VisualEditorViewModel : ActivatableViewModelBase
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
public ObservableCollection<ArtemisDevice> Devices { get; } public ObservableCollection<ArtemisDevice> Devices { get; }
public ObservableCollection<IVisualizerViewModel> Visualizers { get; set; } public ReadOnlyObservableCollection<IVisualizerViewModel> Visualizers { get; }
public ObservableCollection<IToolViewModel> Tools { get; set; } public ObservableCollection<IToolViewModel> Tools { get; }
private void CreateVisualizers(ProfileConfiguration? profileConfiguration) private void CreateVisualizers(ProfileConfiguration? profileConfiguration)
{ {
Visualizers.Clear(); _visualizers.Edit(list =>
if (profileConfiguration?.Profile == null) {
return; list.Clear();
if (profileConfiguration?.Profile == null)
foreach (Layer layer in profileConfiguration.Profile.GetAllLayers()) return;
CreateVisualizer(layer); foreach (Layer layer in profileConfiguration.Profile.GetAllLayers())
CreateVisualizer(list, layer);
});
} }
private void CreateVisualizer(Layer layer) private void CreateVisualizer(ICollection<IVisualizerViewModel> visualizerViewModels, Layer layer)
{ {
Visualizers.Add(_vmFactory.LayerVisualizerViewModel(layer)); visualizerViewModels.Add(_vmFactory.LayerShapeVisualizerViewModel(layer));
visualizerViewModels.Add(_vmFactory.LayerVisualizerViewModel(layer));
} }
} }

View File

@ -2,4 +2,7 @@
public interface IVisualizerViewModel public interface IVisualizerViewModel
{ {
int X { get; }
int Y { get; }
int Order { get; }
} }

View File

@ -0,0 +1,48 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:visualizers="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerShapeVisualizerView"
x:DataType="visualizers:LayerShapeVisualizerViewModel"
ClipToBounds="False"
ZIndex="2">
<UserControl.Styles>
<Style Selector="Path.layer-visualizer">
<Setter Property="Stroke" Value="{StaticResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="Path.layer-visualizer-selected">
<Setter Property="Stroke" Value="{StaticResource SystemAccentColorLight1}" />
</Style>
<Style Selector="Path.layer-visualizer-unbound">
<Setter Property="Stroke" Value="{StaticResource ButtonBorderBrushDisabled}" />
<Setter Property="Opacity" Value="0.50" />
</Style>
<Style Selector="Path.layer-visualizer-unbound-selected">
<Setter Property="Opacity" Value="0.75" />
</Style>
</UserControl.Styles>
<Canvas>
<Path Classes="layer-visualizer-unbound"
Classes.layer-visualizer-unbound-selected="{CompiledBinding Selected}"
Data="{CompiledBinding ShapeGeometry, Mode=OneWay}"
StrokeThickness="2"
Margin="0 0 2 2"
StrokeJoin="Round">
</Path>
<Border ClipToBounds="True"
Width="{CompiledBinding LayerBounds.Width}"
Height="{CompiledBinding LayerBounds.Height}">
<Path Classes="layer-visualizer"
Classes.layer-visualizer-selected="{CompiledBinding Selected}"
Data="{CompiledBinding ShapeGeometry, Mode=OneWay}"
StrokeThickness="2"
Margin="0 0 2 2"
StrokeJoin="Round">
</Path>
</Border>
</Canvas>
</UserControl>

View File

@ -0,0 +1,18 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers
{
public partial class LayerShapeVisualizerView : ReactiveUserControl<LayerShapeVisualizerViewModel>
{
public LayerShapeVisualizerView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia;
using Avalonia.Controls.Mixins;
using Avalonia.Media;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel
{
private ObservableAsPropertyHelper<bool>? _selected;
private Geometry? _shapeGeometry;
public LayerShapeVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService)
{
Layer = layer;
this.WhenActivated(d =>
{
Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x).Subscribe(_ => Update()).DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.Position.CurrentValueSet += x, x => Layer.Transform.Position.CurrentValueSet -= x)
.Subscribe(_ => UpdateTransform())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.Rotation.CurrentValueSet += x, x => Layer.Transform.Rotation.CurrentValueSet -= x)
.Subscribe(_ => UpdateTransform())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.Scale.CurrentValueSet += x, x => Layer.Transform.Scale.CurrentValueSet -= x)
.Subscribe(_ => UpdateTransform())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.AnchorPoint.CurrentValueSet += x, x => Layer.Transform.AnchorPoint.CurrentValueSet -= x)
.Subscribe(_ => UpdateTransform())
.DisposeWith(d);
_selected = profileEditorService.ProfileElement.Select(p => p == Layer).ToProperty(this, vm => vm.Selected).DisposeWith(d);
profileEditorService.Time.Subscribe(_ => UpdateTransform()).DisposeWith(d);
Update();
UpdateTransform();
});
}
public Layer Layer { get; }
public bool Selected => _selected?.Value ?? false;
public Rect LayerBounds => Layer.Bounds.ToRect();
public Geometry? ShapeGeometry
{
get => _shapeGeometry;
set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value);
}
private void Update()
{
if (Layer.General.ShapeType.CurrentValue == LayerShapeType.Rectangle)
ShapeGeometry = new RectangleGeometry(new Rect(0, 0, Layer.Bounds.Width, Layer.Bounds.Height));
else
ShapeGeometry = new EllipseGeometry(new Rect(0, 0, Layer.Bounds.Width, Layer.Bounds.Height));
this.RaisePropertyChanged(nameof(X));
this.RaisePropertyChanged(nameof(Y));
this.RaisePropertyChanged(nameof(LayerBounds));
}
private void UpdateTransform()
{
if (ShapeGeometry != null)
ShapeGeometry.Transform = new MatrixTransform(Layer.GetTransformMatrix(true, true, true, true).ToMatrix());
}
public int X => Layer.Bounds.Left;
public int Y => Layer.Bounds.Top;
public int Order => 2;
}

View File

@ -5,19 +5,24 @@
xmlns:visualizers="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers" xmlns:visualizers="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerVisualizerView" x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerVisualizerView"
x:DataType="visualizers:LayerVisualizerViewModel"> x:DataType="visualizers:LayerVisualizerViewModel"
ZIndex="1">
<UserControl.Styles> <UserControl.Styles>
<Style Selector="Path.layer-visualizer"> <Style Selector="Path.layer-visualizer">
<Setter Property="Stroke" Value="{StaticResource ButtonBorderBrushDisabled}" /> <Setter Property="Stroke" Value="{StaticResource ButtonBorderBrushDisabled}" />
</Style> </Style>
<Style Selector="Path.layer-visualizer-selected"> <Style Selector="Path.layer-visualizer-selected">
<Setter Property="Stroke" Value="{StaticResource SystemAccentColorLight1}" /> <Setter Property="Stroke" Value="{StaticResource SystemAccentColorDark2}" />
</Style> </Style>
</UserControl.Styles> </UserControl.Styles>
<Path Classes="layer-visualizer" <Path Classes="layer-visualizer"
Classes.layer-visualizer-selected="{CompiledBinding Selected}" Classes.layer-visualizer-selected="{CompiledBinding Selected}"
Data="{CompiledBinding ShapeGeometry, Mode=OneWay}"
StrokeThickness="2" StrokeThickness="2"
Margin="0 0 2 2"> Margin="0 0 2 2"
StrokeDashArray="6,2"
StrokeJoin="Round">
<Path.Data>
<RectangleGeometry Rect="{CompiledBinding LayerBounds}"></RectangleGeometry>
</Path.Data>
</Path> </Path>
</UserControl> </UserControl>

View File

@ -2,66 +2,44 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Media;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel
{ {
private Geometry? _shapeGeometry;
private ObservableAsPropertyHelper<bool>? _selected; private ObservableAsPropertyHelper<bool>? _selected;
public LayerVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService) public LayerVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService)
{ {
Layer = layer; Layer = layer;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x).Subscribe(_ => UpdateShape()).DisposeWith(d); Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x)
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.Position.CurrentValueSet += x, x => Layer.Transform.Position.CurrentValueSet -= x) .Subscribe(_ => Update())
.Subscribe(_ => UpdateTransform())
.DisposeWith(d); .DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.Rotation.CurrentValueSet += x, x => Layer.Transform.Rotation.CurrentValueSet -= x) _selected = profileEditorService.ProfileElement
.Subscribe(_ => UpdateTransform()) .Select(p => p == Layer)
.ToProperty(this, vm => vm.Selected)
.DisposeWith(d); .DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.Scale.CurrentValueSet += x, x => Layer.Transform.Scale.CurrentValueSet -= x)
.Subscribe(_ => UpdateTransform())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => Layer.Transform.AnchorPoint.CurrentValueSet += x, x => Layer.Transform.AnchorPoint.CurrentValueSet -= x)
.Subscribe(_ => UpdateTransform())
.DisposeWith(d);
_selected = profileEditorService.ProfileElement.Select(p => p == Layer).ToProperty(this, vm => vm.Selected).DisposeWith(d);
profileEditorService.Time.Subscribe(_ => UpdateTransform()).DisposeWith(d);
UpdateShape();
}); });
} }
public Layer Layer { get; } public Layer Layer { get; }
public bool Selected => _selected?.Value ?? false; public bool Selected => _selected?.Value ?? false;
public Rect LayerBounds => new(0, 0, Layer.Bounds.Width, Layer.Bounds.Height);
public Geometry? ShapeGeometry private void Update()
{ {
get => _shapeGeometry; this.RaisePropertyChanged(nameof(X));
set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value); this.RaisePropertyChanged(nameof(Y));
this.RaisePropertyChanged(nameof(LayerBounds));
} }
private void UpdateShape() public int X => Layer.Bounds.Left;
{ public int Y => Layer.Bounds.Top;
if (Layer.General.ShapeType.CurrentValue == LayerShapeType.Rectangle) public int Order => 1;
ShapeGeometry = new RectangleGeometry(Layer.Bounds.ToRect());
else
ShapeGeometry = new EllipseGeometry(Layer.Bounds.ToRect());
}
private void UpdateTransform()
{
if (ShapeGeometry != null)
ShapeGeometry.Transform = new MatrixTransform(Layer.GetTransformMatrix(false, true, true, true).ToMatrix());
}
} }