1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Device visualizer - Added support for rotation and scale

Profile editor - Fixed keyframe context menu actions
This commit is contained in:
SpoinkyNL 2020-06-22 22:14:33 +02:00
parent 8cb5d31175
commit 920aea6695
15 changed files with 185 additions and 78 deletions

View File

@ -30,16 +30,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.0.1" />
<PackageReference Include="Fody" Version="6.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
<PackageReference Include="MaterialDesignExtensions" Version="3.1.0" />
<PackageReference Include="MaterialDesignThemes" Version="3.1.3" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
<PackageReference Include="SkiaSharp" Version="1.68.3" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.3" />
<PackageReference Include="Stylet" Version="1.3.2" />

View File

@ -1,16 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Artemis.Core.Models.Surface;
using RGB.NET.Core;
using Point = System.Windows.Point;
using Size = System.Windows.Size;
namespace Artemis.UI.Shared.Controls
{
@ -29,6 +28,7 @@ namespace Artemis.UI.Shared.Controls
private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds;
private BitmapImage _deviceImage;
private bool _subscribed;
private ArtemisDevice _oldDevice;
public DeviceVisualizer()
{
@ -38,7 +38,7 @@ namespace Artemis.UI.Shared.Controls
Loaded += (sender, args) => SubscribeToUpdate(true);
Unloaded += (sender, args) => SubscribeToUpdate(false);
}
public ArtemisDevice Device
{
get => (ArtemisDevice) GetValue(DeviceProperty);
@ -68,9 +68,10 @@ namespace Artemis.UI.Shared.Controls
return;
// Determine the scale required to fit the desired size of the control
var scale = Math.Min(DesiredSize.Width / Device.RgbDevice.Size.Width, DesiredSize.Height / Device.RgbDevice.Size.Height);
var scaledRect = new Rect(0, 0, Device.RgbDevice.Size.Width * scale, Device.RgbDevice.Size.Height * scale);
var measureSize = MeasureOverride(Size.Empty);
var scale = Math.Min(DesiredSize.Width / measureSize.Width, DesiredSize.Height / measureSize.Height);
var scaledRect = new Rect(0, 0, measureSize.Width * scale, measureSize.Height * scale);
// Center and scale the visualization in the desired bounding box
if (DesiredSize.Width > 0 && DesiredSize.Height > 0)
{
@ -78,16 +79,45 @@ namespace Artemis.UI.Shared.Controls
drawingContext.PushTransform(new ScaleTransform(scale, scale));
}
// Determine the offset required to rotate within bounds
var rotationRect = new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
rotationRect.Transform(new RotateTransform(Device.Rotation).Value);
// Apply device rotation
drawingContext.PushTransform(new TranslateTransform(0 - rotationRect.Left, 0 - rotationRect.Top));
drawingContext.PushTransform(new RotateTransform(Device.Rotation));
// Apply device scale
drawingContext.PushTransform(new ScaleTransform(Device.Scale, Device.Scale));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height));
foreach (var deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderImage(drawingContext);
drawingContext.DrawDrawing(_backingStore);
}
protected override Size MeasureOverride(Size availableSize)
{
if (Device == null)
return Size.Empty;
var rotationRect = new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
rotationRect.Transform(new RotateTransform(Device.Rotation).Value);
return rotationRect.Size;
}
private void UpdateTransform()
{
InvalidateVisual();
InvalidateMeasure();
}
private void SubscribeToUpdate(bool subscribe)
{
if (_subscribed == subscribe)
@ -121,6 +151,13 @@ namespace Artemis.UI.Shared.Controls
if (Device == null)
return;
if (_oldDevice != null)
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
_oldDevice = Device;
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
UpdateTransform();
// Load the device main image
if (Device.RgbDevice?.DeviceInfo?.Image?.AbsolutePath != null && File.Exists(Device.RgbDevice.DeviceInfo.Image.AbsolutePath))
_deviceImage = new BitmapImage(Device.RgbDevice.DeviceInfo.Image);
@ -130,7 +167,10 @@ namespace Artemis.UI.Shared.Controls
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
if (!ShowColors)
{
InvalidateMeasure();
return;
}
// Create the opacity drawing group
var opacityDrawingGroup = new DrawingGroup();
@ -157,6 +197,14 @@ namespace Artemis.UI.Shared.Controls
var bitmapBrush = new ImageBrush(bitmap);
bitmapBrush.Freeze();
_backingStore.OpacityMask = bitmapBrush;
InvalidateMeasure();
}
private void DevicePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Device.RgbDevice.Scale) || e.PropertyName == nameof(Device.RgbDevice.Rotation))
UpdateTransform();
}
private void RgbSurfaceOnUpdated(UpdatedEventArgs e)

View File

@ -13,12 +13,14 @@ namespace Artemis.UI.Shared.Controls
{
public DeviceVisualizerLed(ArtemisLed led)
{
if (led.Device.Scale != 1)
Console.WriteLine();
Led = led;
LedRect = new Rect(
Led.RgbLed.LedRectangle.Location.X,
Led.RgbLed.LedRectangle.Location.Y,
Led.RgbLed.LedRectangle.Size.Width,
Led.RgbLed.LedRectangle.Size.Height
Led.RgbLed.Location.X,
Led.RgbLed.Location.Y,
Led.RgbLed.Size.Width,
Led.RgbLed.Size.Height
);
if (Led.RgbLed.Image != null && File.Exists(Led.RgbLed.Image.AbsolutePath))
@ -97,7 +99,7 @@ namespace Artemis.UI.Shared.Controls
}
// Stroke geometry is the display geometry excluding the inner geometry
DisplayGeometry.Transform = new TranslateTransform(Led.RgbLed.LedRectangle.Location.X, Led.RgbLed.LedRectangle.Location.Y);
DisplayGeometry.Transform = new TranslateTransform(Led.RgbLed.Location.X, Led.RgbLed.Location.Y);
// Try to gain some performance
DisplayGeometry.Freeze();
}

View File

@ -12,7 +12,7 @@
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource MaterialDesignValidationErrorTemplate}" />
</Style>
</UserControl.Style>
<StackPanel>
<Grid>
<!-- Drag handle -->
<Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19">
<Border.BorderBrush>
@ -28,8 +28,8 @@
<TextBlock Style="{x:Null}"
Width="60"
Height="17"
Padding="1 0"
Margin="0 4 0 0"
Padding="2 0"
Margin="0 3 0 0"
Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Cursor="/Resources/Cursors/aero_drag_ew.cur"
Foreground="{DynamicResource SecondaryAccentBrush}"
@ -42,11 +42,13 @@
<!-- Input -->
<TextBox x:Name="Input"
Width="60"
Height="20"
Height="21"
Padding="0 0 -2 0"
HorizontalAlignment="Left"
Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
LostFocus="InputLostFocus"
KeyDown="InputKeyDown"
Visibility="Collapsed"
RequestBringIntoView="Input_OnRequestBringIntoView" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -8,6 +8,7 @@ namespace Artemis.UI.Shared.PropertyInput
public abstract class PropertyInputViewModel<T> : ValidatingModelBase, IDisposable
{
private T _inputValue;
private bool _inputDragging;
protected PropertyInputViewModel(LayerProperty<T> layerProperty, IProfileEditorService profileEditorService)
{
@ -30,14 +31,18 @@ namespace Artemis.UI.Shared.PropertyInput
public LayerProperty<T> LayerProperty { get; }
public IProfileEditorService ProfileEditorService { get; }
public bool InputDragging { get; private set; }
public bool InputDragging
{
get => _inputDragging;
private set => SetAndNotify(ref _inputDragging, value);
}
public T InputValue
{
get => _inputValue;
set
{
_inputValue = value;
SetAndNotify(ref _inputValue, value);
ApplyInputValue();
}
}

View File

@ -14,7 +14,7 @@
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}"
TextWrapping="Wrap" />
<Separator Margin="0 15" />
<ScrollViewer MaxHeight="500">
<ScrollViewer MaxHeight="500" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsControl ItemsSource="{Binding Exceptions}">
<ItemsControl.ItemTemplate>

View File

@ -7,6 +7,8 @@ namespace Artemis.UI.Shared.Screens.Dialogs
{
public class ExceptionDialogViewModel : DialogViewModelBase
{
private List<DialogException> _exceptions;
public ExceptionDialogViewModel(string message, Exception exception)
{
Header = message;
@ -21,8 +23,12 @@ namespace Artemis.UI.Shared.Screens.Dialogs
}
public string Header { get; }
public List<DialogException> Exceptions { get; set; }
public List<DialogException> Exceptions
{
get => _exceptions;
set => SetAndNotify(ref _exceptions, value);
}
public void Close()
{

View File

@ -96,18 +96,25 @@ namespace Artemis.UI.Shared.Services.Dialog
new ConstructorArgument("exception", exception)
};
await Execute.OnUIThreadAsync(async () =>
try
{
try
await Execute.OnUIThreadAsync(async () =>
{
DialogHost.CloseDialogCommand.Execute(new object(), null);
await ShowDialog<ExceptionDialogViewModel>(arguments);
}
catch (Exception)
{
// ignored
}
});
try
{
DialogHost.CloseDialogCommand.Execute(new object(), null);
await ShowDialog<ExceptionDialogViewModel>(arguments);
}
catch (Exception)
{
// ignored
}
});
}
catch (Exception)
{
// ignored
}
}
private async Task<object> ShowDialog(string identifier, DialogViewModelBase viewModel)

View File

@ -5,6 +5,9 @@ namespace Artemis.UI.Shared.Services.Dialog
{
public abstract class DialogViewModelBase : ValidatingModelBase
{
private DialogViewModelHost _dialogViewModelHost;
private DialogSession _session;
protected DialogViewModelBase(IModelValidator validator) : base(validator)
{
}
@ -13,8 +16,17 @@ namespace Artemis.UI.Shared.Services.Dialog
{
}
public DialogViewModelHost DialogViewModelHost { get; set; }
public DialogSession Session { get; private set; }
public DialogViewModelHost DialogViewModelHost
{
get => _dialogViewModelHost;
set => SetAndNotify(ref _dialogViewModelHost, value);
}
public DialogSession Session
{
get => _session;
private set => SetAndNotify(ref _session, value);
}
public void OnDialogOpened(object sender, DialogOpenedEventArgs e)
{

View File

@ -23,7 +23,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
#region Context menu actions
public void Copy()
public override void Copy()
{
var newKeyframe = new LayerPropertyKeyframe<T>(
LayerPropertyKeyframe.Value,
@ -35,7 +35,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
_profileEditorService.UpdateSelectedProfileElement();
}
public void Delete()
public override void Delete()
{
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
@ -73,23 +73,13 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
}
#region Keyframe movement
public abstract void Copy();
#endregion
public abstract void Delete();
#region Easing
public void ContextMenuOpening()
{
CreateEasingViewModels();
}
public void ContextMenuClosing()
{
EasingViewModels.Clear();
}
private void CreateEasingViewModels()
public void CreateEasingViewModels()
{
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v)));
}

View File

@ -63,12 +63,12 @@
</Ellipse.Style>
<Ellipse.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy" Command="{s:Action Copy}">
<MenuItem Header="Copy" Command="{s:Action Copy}" CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Delete" Command="{s:Action Delete}">
<MenuItem Header="Delete" Command="{s:Action Delete}" CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>

View File

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Utilities;
@ -58,7 +60,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
if (viewModel == null)
return;
((IInputElement) sender).CaptureMouse();
((IInputElement)sender).CaptureMouse();
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !viewModel.IsSelected)
SelectKeyframe(viewModel, true, false);
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
@ -74,7 +76,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
_profileEditorService.UpdateSelectedProfileElement();
ReleaseSelectedKeyframes();
((IInputElement) sender).ReleaseMouseCapture();
((IInputElement)sender).ReleaseMouseCapture();
}
public void KeyframeMouseMove(object sender, MouseEventArgs e)
@ -85,6 +87,32 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
e.Handled = true;
}
#region Context menu actions
public void ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
viewModel?.CreateEasingViewModels();
}
public void ContextMenuClosing(object sender, ContextMenuEventArgs e)
{
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
viewModel?.EasingViewModels.Clear();
}
public void Copy(TimelineKeyframeViewModel viewModel)
{
viewModel.Copy();
}
public void Delete(TimelineKeyframeViewModel viewModel)
{
viewModel.Delete();
}
#endregion
private TimeSpan GetCursorTime(Point position)
{
// Get the parent grid, need that for our position
@ -148,10 +176,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
if (e.LeftButton == MouseButtonState.Released)
return;
((IInputElement) sender).CaptureMouse();
((IInputElement)sender).CaptureMouse();
SelectionRectangle.Rect = new Rect();
_mouseDragStartPoint = e.GetPosition((IInputElement) sender);
_mouseDragStartPoint = e.GetPosition((IInputElement)sender);
_mouseDragging = true;
e.Handled = true;
}
@ -162,25 +190,25 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
if (!_mouseDragging)
return;
var position = e.GetPosition((IInputElement) sender);
var position = e.GetPosition((IInputElement)sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
var keyframeViewModels = GetAllKeyframeViewModels();
var selectedKeyframes = HitTestUtilities.GetHitViewModels<TimelineKeyframeViewModel>((Visual) sender, SelectionRectangle);
var selectedKeyframes = HitTestUtilities.GetHitViewModels<TimelineKeyframeViewModel>((Visual)sender, SelectionRectangle);
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = selectedKeyframes.Contains(keyframeViewModel);
_mouseDragging = false;
e.Handled = true;
((IInputElement) sender).ReleaseMouseCapture();
((IInputElement)sender).ReleaseMouseCapture();
}
public void TimelineCanvasMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDragging && e.LeftButton == MouseButtonState.Pressed)
{
var position = e.GetPosition((IInputElement) sender);
var position = e.GetPosition((IInputElement)sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
e.Handled = true;
@ -236,7 +264,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
viewModels.AddRange(layerPropertyGroupViewModel.GetAllChildren());
var keyframes = viewModels.Where(vm => vm is LayerPropertyViewModel)
.SelectMany(vm => ((LayerPropertyViewModel) vm).TimelinePropertyBaseViewModel.TimelineKeyframeViewModels)
.SelectMany(vm => ((LayerPropertyViewModel)vm).TimelinePropertyBaseViewModel.TimelineKeyframeViewModels)
.ToList();
return keyframes;

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Artemis.Core.Services.Interfaces;
using Artemis.UI.Screens.SurfaceEditor.Visualization;
using Artemis.UI.Shared.Services.Dialog;
using Stylet;
@ -7,9 +8,12 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs
{
public class SurfaceDeviceConfigViewModel : DialogViewModelBase
{
public SurfaceDeviceConfigViewModel(SurfaceDeviceViewModel surfaceDeviceViewModel, IModelValidator<SurfaceDeviceConfigViewModel> validator)
private readonly ICoreService _coreService;
public SurfaceDeviceConfigViewModel(SurfaceDeviceViewModel surfaceDeviceViewModel, ICoreService coreService, IModelValidator<SurfaceDeviceConfigViewModel> validator)
: base(validator)
{
_coreService = coreService;
SurfaceDeviceViewModel = surfaceDeviceViewModel;
Title = $"{SurfaceDeviceViewModel.Device.RgbDevice.DeviceInfo.DeviceName} - Properties";
@ -33,11 +37,16 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs
if (HasErrors)
return;
_coreService.ModuleRenderingDisabled = true;
await Task.Delay(100);
SurfaceDeviceViewModel.Device.X = X;
SurfaceDeviceViewModel.Device.Y = Y;
SurfaceDeviceViewModel.Device.Scale = Scale;
SurfaceDeviceViewModel.Device.Rotation = Rotation;
_coreService.ModuleRenderingDisabled = false;
Session.Close(true);
}

View File

@ -21,12 +21,8 @@
</UserControl.Resources>
<Grid>
<!-- Content -->
<Grid Width="{Binding Device.RgbDevice.ActualSize.Width}" Height="{Binding Device.RgbDevice.ActualSize.Height}">
<Grid.RenderTransform>
<RotateTransform Angle="{Binding Device.Rotation}" />
</Grid.RenderTransform>
<controls:DeviceVisualizer Device="{Binding Device}" />
<Grid>
<controls:DeviceVisualizer Device="{Binding Device}" VerticalAlignment="Top" HorizontalAlignment="Left"/>
<Rectangle Fill="{DynamicResource MaterialDesignCardBackground}"
Stroke="{DynamicResource MaterialDesignTextBoxBorder}"

View File

@ -6,17 +6,19 @@
<Lighting>Key</Lighting>
<Vendor>Corsair</Vendor>
<Model>H115i RGB Pump</Model>
<Width>80</Width>
<Height>80</Height>
<LedUnitWidth>62</LedUnitWidth>
<LedUnitHeight>63</LedUnitHeight>
<Width>60</Width>
<Height>60</Height>
<LedUnitWidth>1</LedUnitWidth>
<LedUnitHeight>1</LedUnitHeight>
<ImageBasePath>Images\Corsair\Customs</ImageBasePath>
<DeviceImage>PUMP.png</DeviceImage>
<Leds>
<Led Id="Cooler1">
<Shape> M0.192,0.053 L0.115,0.114 L0.047,0.194 L0.034,0.499 L0.048,0.828 L0.119,0.91 L0.197,0.97 L0.508,0.978 L0.828,0.965 L0.905,0.903 L0.971,0.831 L0.983,0.538 L0.974,0.197 L0.916,0.117 L0.834,0.05 L0.59,0.038 L0.29,0.042z M0.208,0.091 L0.135,0.149 L0.091,0.208 L0.078,0.503 L0.091,0.815 L0.146,0.877 L0.21,0.924 L0.493,0.935 L0.809,0.924 L0.875,0.868 L0.93,0.807 L0.943,0.537 L0.932,0.21 L0.875,0.138 L0.813,0.09 L0.587,0.079 L0.305,0.081z M0.388,0.429 L0.403,0.491 L0.391,0.544 L0.369,0.582 L0.431,0.554 L0.53,0.524 L0.609,0.519 L0.625,0.527 L0.636,0.542 L0.648,0.531 L0.654,0.503 L0.649,0.481 L0.627,0.45 L0.574,0.407 L0.509,0.36 L0.521,0.393 L0.526,0.443 L0.519,0.492 L0.512,0.509 L0.515,0.477 L0.517,0.435 L0.45,0.394 L0.459,0.429 L0.462,0.477 L0.448,0.521 L0.455,0.485 L0.454,0.457 L0.397,0.425z M0.404,0.389 L0.414,0.407 L0.418,0.433 L0.451,0.44 L0.449,0.428 L0.439,0.405z M0.459,0.351 L0.471,0.373 L0.477,0.405 L0.512,0.414 L0.514,0.405 L0.514,0.405 L0.499,0.372z M0.519,0.307 L0.533,0.34 L0.541,0.374 L0.567,0.383 L0.593,0.402 L0.605,0.42 L0.612,0.415 L0.613,0.398 L0.597,0.362 L0.546,0.317z M0.324,0.632 L0.323,0.61 L0.308,0.603 L0.276,0.602 L0.254,0.618 L0.245,0.645 L0.253,0.673 L0.275,0.69 L0.309,0.691 L0.327,0.682 L0.329,0.656 L0.322,0.658 L0.312,0.675 L0.283,0.676 L0.267,0.656 L0.266,0.634 L0.28,0.616 L0.298,0.612 L0.314,0.62z M0.377,0.604 L0.358,0.605 L0.338,0.623 L0.333,0.651 L0.344,0.677 L0.363,0.69 L0.393,0.69 L0.413,0.674 L0.422,0.647 L0.418,0.624 L0.403,0.607z M0.377,0.612 L0.359,0.625 L0.353,0.65 L0.361,0.673 L0.387,0.674 L0.4,0.656 L0.4,0.637 L0.391,0.619z M0.419,0.603 L0.427,0.613 L0.427,0.677 L0.42,0.69 L0.453,0.69 L0.446,0.679 L0.447,0.608 L0.461,0.61 L0.468,0.624 L0.466,0.641 L0.45,0.65 L0.466,0.675 L0.48,0.691 L0.496,0.697 L0.506,0.695 L0.497,0.687 L0.48,0.659 L0.473,0.648 L0.469,0.646 L0.481,0.646 L0.488,0.633 L0.488,0.619 L0.482,0.604 L0.457,0.599 L0.433,0.602z M0.556,0.624 L0.554,0.609 L0.541,0.6 L0.523,0.6 L0.506,0.611 L0.502,0.634 L0.513,0.648 L0.531,0.657 L0.541,0.666 L0.538,0.675 L0.523,0.68 L0.512,0.671 L0.505,0.661 L0.5,0.662 L0.502,0.681 L0.52,0.69 L0.546,0.687 L0.557,0.677 L0.562,0.663 L0.558,0.65 L0.545,0.639 L0.527,0.632 L0.518,0.623 L0.523,0.611 L0.538,0.61z M0.595,0.603 L0.566,0.677 L0.555,0.692 L0.587,0.691 L0.578,0.68 L0.584,0.663 L0.609,0.664 L0.617,0.679 L0.609,0.689 L0.645,0.689 L0.638,0.677 L0.606,0.603z M0.646,0.604 L0.653,0.612 L0.654,0.677 L0.647,0.689 L0.681,0.689 L0.675,0.677 L0.675,0.612 L0.684,0.603z M0.688,0.603 L0.691,0.612 L0.691,0.681 L0.686,0.69 L0.72,0.69 L0.715,0.678 L0.712,0.674 L0.712,0.613 L0.719,0.61 L0.731,0.615 L0.735,0.634 L0.718,0.651 L0.738,0.683 L0.754,0.695 L0.774,0.695 L0.761,0.684 L0.741,0.65 L0.737,0.648 L0.748,0.648 L0.757,0.629 L0.755,0.613 L0.739,0.602 L0.707,0.601z</Shape>
<X>8.6</X>
<Y>7.2</Y>
<X>6</X>
<Y>5.4</Y>
<Width>47.5</Width>
<Height>47.5</Height>
</Led>
</Leds>
<LedImageLayouts>
@ -25,5 +27,10 @@
<LedImage Id="Cooler1" />
</LedImages>
</LedImageLayout>
<LedImageLayout>
<LedImages>
<LedImage Id="Cooler1" />
</LedImages>
</LedImageLayout>
</LedImageLayouts>
</Device>