diff --git a/Minint.Core/Services/IImageEffectsService.cs b/Minint.Core/Services/IImageEffectsService.cs
new file mode 100644
index 0000000..570c6c2
--- /dev/null
+++ b/Minint.Core/Services/IImageEffectsService.cs
@@ -0,0 +1,18 @@
+using Minint.Core.Models;
+
+namespace Minint.Core.Services;
+
+public interface IImageEffectsService
+{
+ ///
+ /// Adjusts contrast of the document by transforming its palette colors.
+ /// of 0 = all gray, 1 = no change, >1 = increased contrast.
+ ///
+ void ApplyContrast(MinintDocument doc, double factor);
+
+ ///
+ /// Converts the document to grayscale by transforming its palette colors
+ /// using the luminance formula: 0.299R + 0.587G + 0.114B.
+ ///
+ void ApplyGrayscale(MinintDocument doc);
+}
diff --git a/Minint.Core/Services/Impl/ImageEffectsService.cs b/Minint.Core/Services/Impl/ImageEffectsService.cs
new file mode 100644
index 0000000..67c25f6
--- /dev/null
+++ b/Minint.Core/Services/Impl/ImageEffectsService.cs
@@ -0,0 +1,38 @@
+using System;
+using Minint.Core.Models;
+
+namespace Minint.Core.Services.Impl;
+
+public sealed class ImageEffectsService : IImageEffectsService
+{
+ public void ApplyContrast(MinintDocument doc, double factor)
+ {
+ for (int i = 1; i < doc.Palette.Count; i++)
+ {
+ var c = doc.Palette[i];
+ doc.Palette[i] = new RgbaColor(
+ ContrastByte(c.R, factor),
+ ContrastByte(c.G, factor),
+ ContrastByte(c.B, factor),
+ c.A);
+ }
+ doc.InvalidatePaletteCache();
+ }
+
+ public void ApplyGrayscale(MinintDocument doc)
+ {
+ for (int i = 1; i < doc.Palette.Count; i++)
+ {
+ var c = doc.Palette[i];
+ byte gray = (byte)Math.Clamp((int)(0.299 * c.R + 0.587 * c.G + 0.114 * c.B + 0.5), 0, 255);
+ doc.Palette[i] = new RgbaColor(gray, gray, gray, c.A);
+ }
+ doc.InvalidatePaletteCache();
+ }
+
+ private static byte ContrastByte(byte value, double factor)
+ {
+ double v = ((value / 255.0) - 0.5) * factor + 0.5;
+ return (byte)Math.Clamp((int)(v * 255 + 0.5), 0, 255);
+ }
+}
diff --git a/Minint.Core/Services/Impl/PatternGenerator.cs b/Minint.Core/Services/Impl/PatternGenerator.cs
new file mode 100644
index 0000000..8561b47
--- /dev/null
+++ b/Minint.Core/Services/Impl/PatternGenerator.cs
@@ -0,0 +1,113 @@
+using System;
+using Minint.Core.Models;
+
+namespace Minint.Core.Services.Impl;
+
+public sealed class PatternGenerator : IPatternGenerator
+{
+ public MinintDocument Generate(PatternType type, int width, int height, RgbaColor[] colors, int param1, int param2 = 0)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(width, 1);
+ ArgumentOutOfRangeException.ThrowIfLessThan(height, 1);
+ if (colors.Length < 2)
+ throw new ArgumentException("At least two colors are required.", nameof(colors));
+
+ var doc = new MinintDocument($"Pattern ({type})");
+ var layer = new MinintLayer("Pattern", width * height);
+ doc.Layers.Add(layer);
+
+ int[] colorIndices = new int[colors.Length];
+ for (int i = 0; i < colors.Length; i++)
+ colorIndices[i] = doc.EnsureColorCached(colors[i]);
+
+ int cellSize = Math.Max(param1, 1);
+
+ switch (type)
+ {
+ case PatternType.Checkerboard:
+ FillCheckerboard(layer.Pixels, width, height, colorIndices, cellSize);
+ break;
+ case PatternType.HorizontalGradient:
+ FillGradient(layer.Pixels, width, height, colors[0], colors[1], doc, horizontal: true);
+ break;
+ case PatternType.VerticalGradient:
+ FillGradient(layer.Pixels, width, height, colors[0], colors[1], doc, horizontal: false);
+ break;
+ case PatternType.HorizontalStripes:
+ FillStripes(layer.Pixels, width, height, colorIndices, cellSize, horizontal: true);
+ break;
+ case PatternType.VerticalStripes:
+ FillStripes(layer.Pixels, width, height, colorIndices, cellSize, horizontal: false);
+ break;
+ case PatternType.ConcentricCircles:
+ FillCircles(layer.Pixels, width, height, colorIndices, cellSize);
+ break;
+ case PatternType.Tile:
+ FillTile(layer.Pixels, width, height, colorIndices, cellSize, Math.Max(param2, 1));
+ break;
+ }
+
+ return doc;
+ }
+
+ private static void FillCheckerboard(int[] pixels, int w, int h, int[] ci, int cell)
+ {
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++)
+ pixels[y * w + x] = ci[((x / cell) + (y / cell)) % 2 == 0 ? 0 : 1];
+ }
+
+ private static void FillGradient(int[] pixels, int w, int h, RgbaColor c0, RgbaColor c1,
+ MinintDocument doc, bool horizontal)
+ {
+ int steps = horizontal ? w : h;
+ for (int s = 0; s < steps; s++)
+ {
+ double t = steps > 1 ? (double)s / (steps - 1) : 0;
+ var c = new RgbaColor(
+ Lerp(c0.R, c1.R, t), Lerp(c0.G, c1.G, t),
+ Lerp(c0.B, c1.B, t), Lerp(c0.A, c1.A, t));
+ int idx = doc.EnsureColorCached(c);
+ if (horizontal)
+ for (int y = 0; y < h; y++) pixels[y * w + s] = idx;
+ else
+ for (int x = 0; x < w; x++) pixels[s * w + x] = idx;
+ }
+ }
+
+ private static void FillStripes(int[] pixels, int w, int h, int[] ci, int stripe, bool horizontal)
+ {
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++)
+ {
+ int coord = horizontal ? y : x;
+ pixels[y * w + x] = ci[(coord / stripe) % ci.Length];
+ }
+ }
+
+ private static void FillCircles(int[] pixels, int w, int h, int[] ci, int ringWidth)
+ {
+ double cx = (w - 1) / 2.0, cy = (h - 1) / 2.0;
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++)
+ {
+ double dist = Math.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
+ int ring = (int)(dist / ringWidth);
+ pixels[y * w + x] = ci[ring % ci.Length];
+ }
+ }
+
+ private static void FillTile(int[] pixels, int w, int h, int[] ci, int tileW, int tileH)
+ {
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++)
+ {
+ int tx = (x / tileW) % ci.Length;
+ int ty = (y / tileH) % ci.Length;
+ pixels[y * w + x] = ci[(tx + ty) % ci.Length];
+ }
+ }
+
+ private static byte Lerp(byte a, byte b, double t)
+ => (byte)Math.Clamp((int)(a + (b - a) * t + 0.5), 0, 255);
+}
diff --git a/Minint/Controls/PixelCanvas.cs b/Minint/Controls/PixelCanvas.cs
index 9023710..9ca6b70 100644
--- a/Minint/Controls/PixelCanvas.cs
+++ b/Minint/Controls/PixelCanvas.cs
@@ -56,7 +56,10 @@ public class PixelCanvas : Control
private Point _panStart;
private double _panStartOffsetX, _panStartOffsetY;
private bool _viewportInitialized;
+ private int _lastBitmapWidth;
+ private int _lastBitmapHeight;
private (int X, int Y)? _lastCursorPixel;
+ private Point? _lastScreenPos;
private ScrollBar? _hScrollBar;
private ScrollBar? _vScrollBar;
@@ -248,6 +251,7 @@ public class PixelCanvas : Control
if (_suppressScrollSync) return;
var (imgW, imgH) = GetImageSize();
_viewport.SetOffset(-e.NewValue, _viewport.OffsetY, imgW, imgH, Bounds.Width, Bounds.Height);
+ RecalcCursorPixel();
InvalidateVisual();
}
@@ -256,6 +260,7 @@ public class PixelCanvas : Control
if (_suppressScrollSync) return;
var (imgW, imgH) = GetImageSize();
_viewport.SetOffset(_viewport.OffsetX, -e.NewValue, imgW, imgH, Bounds.Width, Bounds.Height);
+ RecalcCursorPixel();
InvalidateVisual();
}
@@ -279,6 +284,20 @@ public class PixelCanvas : Control
return (px, py);
}
+ ///
+ /// Recalculates the pixel coordinate under the cursor after a viewport change.
+ ///
+ private void RecalcCursorPixel()
+ {
+ if (_lastScreenPos is null) return;
+ var pixel = ScreenToPixelClamped(_lastScreenPos.Value);
+ if (pixel != _lastCursorPixel)
+ {
+ _lastCursorPixel = pixel;
+ CursorPixelChanged?.Invoke(pixel);
+ }
+ }
+
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
@@ -305,6 +324,7 @@ public class PixelCanvas : Control
_viewport.Pan(dx, dy, imgW, imgH, Bounds.Width, Bounds.Height);
}
+ RecalcCursorPixel();
InvalidateVisual();
e.Handled = true;
}
@@ -338,6 +358,7 @@ public class PixelCanvas : Control
{
base.OnPointerMoved(e);
var pos = e.GetPosition(this);
+ _lastScreenPos = pos;
if (_isPanning)
{
@@ -346,12 +367,12 @@ public class PixelCanvas : Control
_panStartOffsetX + (pos.X - _panStart.X),
_panStartOffsetY + (pos.Y - _panStart.Y),
imgW, imgH, Bounds.Width, Bounds.Height);
+ RecalcCursorPixel();
InvalidateVisual();
e.Handled = true;
return;
}
- // Update preview cursor position
var pixel = ScreenToPixelClamped(pos);
if (pixel != _lastCursorPixel)
{
@@ -386,6 +407,7 @@ public class PixelCanvas : Control
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
+ _lastScreenPos = null;
if (_lastCursorPixel is not null)
{
_lastCursorPixel = null;
@@ -400,6 +422,16 @@ public class PixelCanvas : Control
{
base.OnPropertyChanged(change);
if (change.Property == SourceBitmapProperty)
- _viewportInitialized = false;
+ {
+ var bmp = change.GetNewValue();
+ int w = bmp?.PixelSize.Width ?? 0;
+ int h = bmp?.PixelSize.Height ?? 0;
+ if (w != _lastBitmapWidth || h != _lastBitmapHeight)
+ {
+ _lastBitmapWidth = w;
+ _lastBitmapHeight = h;
+ _viewportInitialized = false;
+ }
+ }
}
}
diff --git a/Minint/Program.cs b/Minint/Program.cs
index f061ea8..26aeca2 100644
--- a/Minint/Program.cs
+++ b/Minint/Program.cs
@@ -28,7 +28,8 @@ sealed class Program
=> AppBuilder.Configure()
.UsePlatformDetect()
.WithInterFont()
- .LogToTrace();
+ .LogToTrace()
+ .With(new X11PlatformOptions { OverlayPopups = true });
// TODO: temporary tests — remove after verification stages.
diff --git a/Minint/ViewModels/EditorViewModel.cs b/Minint/ViewModels/EditorViewModel.cs
index dd21276..42c91be 100644
--- a/Minint/ViewModels/EditorViewModel.cs
+++ b/Minint/ViewModels/EditorViewModel.cs
@@ -113,6 +113,12 @@ public partial class EditorViewModel : ViewModelBase
SyncLayersAndCanvas(doc);
}
+ ///
+ /// Re-syncs the Documents observable collection after an external modification
+ /// to Container.Documents (e.g. pattern generation adding a document).
+ ///
+ public void SyncAfterExternalChange() => SyncDocumentsList();
+
private void SyncDocumentsList()
{
Documents.Clear();
diff --git a/Minint/ViewModels/MainWindowViewModel.cs b/Minint/ViewModels/MainWindowViewModel.cs
index 887c256..15fed79 100644
--- a/Minint/ViewModels/MainWindowViewModel.cs
+++ b/Minint/ViewModels/MainWindowViewModel.cs
@@ -6,13 +6,19 @@ using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using Minint.Core.Models;
+using Minint.Core.Services;
+using Minint.Core.Services.Impl;
using Minint.Infrastructure.Serialization;
+using Minint.Views;
namespace Minint.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly MinintSerializer _serializer = new();
+ private readonly IImageEffectsService _effects = new ImageEffectsService();
+ private readonly IPatternGenerator _patternGen = new PatternGenerator();
private static readonly FilePickerFileType MinintFileType = new("Minint Files")
{
@@ -25,11 +31,10 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty]
private string _statusText = "Ready";
- ///
- /// Set by the view so that file dialogs can use the correct parent window.
- ///
public TopLevel? Owner { get; set; }
+ #region File commands
+
[RelayCommand]
private void NewFile()
{
@@ -72,13 +77,9 @@ public partial class MainWindowViewModel : ViewModelBase
if (Editor.Container is null) return;
if (Editor.FilePath is not null)
- {
await SaveToPathAsync(Editor.FilePath);
- }
else
- {
await SaveFileAsAsync();
- }
}
[RelayCommand]
@@ -121,4 +122,111 @@ public partial class MainWindowViewModel : ViewModelBase
StatusText = $"Error saving file: {ex.Message}";
}
}
+
+ #endregion
+
+ #region Effects (A1, A2)
+
+ [RelayCommand]
+ private async Task ApplyContrastAsync()
+ {
+ if (Editor.ActiveDocument is null || Owner is not Window window) return;
+
+ var dialog = new ContrastDialog();
+ var result = await dialog.ShowDialog(window);
+ if (result != true) return;
+
+ _effects.ApplyContrast(Editor.ActiveDocument, dialog.Factor);
+ Editor.RefreshCanvas();
+ StatusText = $"Contrast ×{dialog.Factor:F1} applied.";
+ }
+
+ [RelayCommand]
+ private void ApplyGrayscale()
+ {
+ if (Editor.ActiveDocument is null) return;
+
+ _effects.ApplyGrayscale(Editor.ActiveDocument);
+ Editor.RefreshCanvas();
+ StatusText = "Grayscale applied.";
+ }
+
+ #endregion
+
+ #region Copy fragment (A4)
+
+ [RelayCommand]
+ private async Task CopyFragmentAsync()
+ {
+ if (Editor.Container is null || Owner is not Window window) return;
+
+ var docs = Editor.Container.Documents;
+ if (docs.Count == 0) return;
+
+ var dialog = new CopyFragmentDialog(docs, Editor.Container.Width, Editor.Container.Height);
+ var result = await dialog.ShowDialog(window);
+ if (result != true) return;
+
+ if (dialog.SourceDocument is null || dialog.DestDocument is null)
+ {
+ StatusText = "Copy failed: no document selected.";
+ return;
+ }
+ if (dialog.SourceLayerIndex < 0 || dialog.DestLayerIndex < 0)
+ {
+ StatusText = "Copy failed: no layer selected.";
+ return;
+ }
+
+ try
+ {
+ Editor.CopyFragment(
+ dialog.SourceDocument, dialog.SourceLayerIndex,
+ dialog.SourceX, dialog.SourceY,
+ dialog.FragmentWidth, dialog.FragmentHeight,
+ dialog.DestDocument, dialog.DestLayerIndex,
+ dialog.DestX, dialog.DestY);
+ StatusText = $"Copied {dialog.FragmentWidth}×{dialog.FragmentHeight} fragment.";
+ }
+ catch (Exception ex)
+ {
+ StatusText = $"Copy failed: {ex.Message}";
+ }
+ }
+
+ #endregion
+
+ #region Pattern generation (Б4)
+
+ [RelayCommand]
+ private async Task GeneratePatternAsync()
+ {
+ if (Editor.Container is null || Owner is not Window window) return;
+
+ var dialog = new PatternDialog();
+ var result = await dialog.ShowDialog(window);
+ if (result != true) return;
+
+ try
+ {
+ var doc = _patternGen.Generate(
+ dialog.SelectedPattern,
+ Editor.Container.Width,
+ Editor.Container.Height,
+ [dialog.PatternColor1, dialog.PatternColor2],
+ dialog.PatternParam1,
+ dialog.PatternParam2);
+
+ Editor.Container.Documents.Add(doc);
+ Editor.SyncAfterExternalChange();
+ Editor.SelectDocument(doc);
+ StatusText = $"Pattern '{dialog.SelectedPattern}' generated.";
+ }
+ catch (Exception ex)
+ {
+ StatusText = $"Pattern generation failed: {ex.Message}";
+ }
+ }
+
+ #endregion
}
diff --git a/Minint/Views/ContrastDialog.axaml b/Minint/Views/ContrastDialog.axaml
new file mode 100644
index 0000000..36b64b2
--- /dev/null
+++ b/Minint/Views/ContrastDialog.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Minint/Views/ContrastDialog.axaml.cs b/Minint/Views/ContrastDialog.axaml.cs
new file mode 100644
index 0000000..86cb19f
--- /dev/null
+++ b/Minint/Views/ContrastDialog.axaml.cs
@@ -0,0 +1,24 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace Minint.Views;
+
+public partial class ContrastDialog : Window
+{
+ public double Factor => FactorSlider.Value;
+
+ public ContrastDialog()
+ {
+ InitializeComponent();
+
+ FactorSlider.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == Slider.ValueProperty)
+ FactorLabel.Text = FactorSlider.Value.ToString("F1");
+ };
+
+ OkButton.Click += (_, _) => Close(true);
+ CancelButton.Click += (_, _) => Close(false);
+ }
+}
diff --git a/Minint/Views/CopyFragmentDialog.axaml b/Minint/Views/CopyFragmentDialog.axaml
new file mode 100644
index 0000000..6168d92
--- /dev/null
+++ b/Minint/Views/CopyFragmentDialog.axaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Minint/Views/CopyFragmentDialog.axaml.cs b/Minint/Views/CopyFragmentDialog.axaml.cs
new file mode 100644
index 0000000..f9a1e6d
--- /dev/null
+++ b/Minint/Views/CopyFragmentDialog.axaml.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Minint.Core.Models;
+
+namespace Minint.Views;
+
+public partial class CopyFragmentDialog : Window
+{
+ private readonly List _documents;
+
+ public MinintDocument? SourceDocument => SrcDocCombo.SelectedItem as MinintDocument;
+ public int SourceLayerIndex => SrcLayerCombo.SelectedIndex;
+ public int SourceX => (int)(SrcX.Value ?? 0);
+ public int SourceY => (int)(SrcY.Value ?? 0);
+ public int FragmentWidth => (int)(RegionW.Value ?? 1);
+ public int FragmentHeight => (int)(RegionH.Value ?? 1);
+ public MinintDocument? DestDocument => DstDocCombo.SelectedItem as MinintDocument;
+ public int DestLayerIndex => DstLayerCombo.SelectedIndex;
+ public int DestX => (int)(DstX.Value ?? 0);
+ public int DestY => (int)(DstY.Value ?? 0);
+
+ public CopyFragmentDialog()
+ {
+ _documents = [];
+ InitializeComponent();
+ }
+
+ public CopyFragmentDialog(List documents, int maxW, int maxH) : this()
+ {
+ _documents = documents;
+
+ SrcDocCombo.ItemsSource = documents;
+ SrcDocCombo.DisplayMemberBinding = new Avalonia.Data.Binding("Name");
+ DstDocCombo.ItemsSource = documents;
+ DstDocCombo.DisplayMemberBinding = new Avalonia.Data.Binding("Name");
+
+ SrcX.Maximum = maxW - 1;
+ SrcY.Maximum = maxH - 1;
+ DstX.Maximum = maxW - 1;
+ DstY.Maximum = maxH - 1;
+ RegionW.Maximum = maxW;
+ RegionH.Maximum = maxH;
+
+ if (documents.Count > 0)
+ {
+ SrcDocCombo.SelectedIndex = 0;
+ DstDocCombo.SelectedIndex = documents.Count > 1 ? 1 : 0;
+ }
+
+ SrcDocCombo.SelectionChanged += (_, _) => UpdateSrcLayers();
+ DstDocCombo.SelectionChanged += (_, _) => UpdateDstLayers();
+
+ UpdateSrcLayers();
+ UpdateDstLayers();
+
+ OkButton.Click += OnOkClick;
+ CancelButton.Click += OnCancelClick;
+ }
+
+ private void UpdateSrcLayers()
+ {
+ if (SrcDocCombo.SelectedItem is MinintDocument doc)
+ {
+ SrcLayerCombo.ItemsSource = doc.Layers;
+ SrcLayerCombo.DisplayMemberBinding = new Avalonia.Data.Binding("Name");
+ if (doc.Layers.Count > 0) SrcLayerCombo.SelectedIndex = 0;
+ }
+ }
+
+ private void UpdateDstLayers()
+ {
+ if (DstDocCombo.SelectedItem is MinintDocument doc)
+ {
+ DstLayerCombo.ItemsSource = doc.Layers;
+ DstLayerCombo.DisplayMemberBinding = new Avalonia.Data.Binding("Name");
+ if (doc.Layers.Count > 0) DstLayerCombo.SelectedIndex = 0;
+ }
+ }
+
+ private void OnOkClick(object? sender, RoutedEventArgs e)
+ {
+ Close(true);
+ }
+
+ private void OnCancelClick(object? sender, RoutedEventArgs e)
+ {
+ Close(false);
+ }
+}
diff --git a/Minint/Views/MainWindow.axaml b/Minint/Views/MainWindow.axaml
index 2c7115d..7db5281 100644
--- a/Minint/Views/MainWindow.axaml
+++ b/Minint/Views/MainWindow.axaml
@@ -9,7 +9,8 @@
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="{Binding Editor.Title}"
- Width="1024" Height="700">
+ Width="1024" Height="700"
+ ToolTip.ShowDelay="400">
@@ -25,6 +26,19 @@
+
+
+
+
+
+
+
@@ -83,13 +97,13 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Minint/Views/PatternDialog.axaml.cs b/Minint/Views/PatternDialog.axaml.cs
new file mode 100644
index 0000000..b4a2d7a
--- /dev/null
+++ b/Minint/Views/PatternDialog.axaml.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Minint.Core.Models;
+using Minint.Core.Services;
+
+namespace Minint.Views;
+
+public partial class PatternDialog : Window
+{
+ public PatternType SelectedPattern =>
+ PatternCombo.SelectedItem is PatternType pt ? pt : PatternType.Checkerboard;
+
+ public RgbaColor PatternColor1
+ {
+ get
+ {
+ var c = Color1Picker.Color;
+ return new RgbaColor(c.R, c.G, c.B, c.A);
+ }
+ }
+
+ public RgbaColor PatternColor2
+ {
+ get
+ {
+ var c = Color2Picker.Color;
+ return new RgbaColor(c.R, c.G, c.B, c.A);
+ }
+ }
+
+ public int PatternParam1 => (int)(Param1.Value ?? 8);
+ public int PatternParam2 => (int)(Param2.Value ?? 8);
+
+ public PatternDialog()
+ {
+ InitializeComponent();
+
+ PatternCombo.ItemsSource = Enum.GetValues().ToList();
+ PatternCombo.SelectedIndex = 0;
+
+ Color1Picker.Color = Color.FromRgb(0, 0, 0);
+ Color2Picker.Color = Color.FromRgb(255, 255, 255);
+
+ OkButton.Click += (_, _) => Close(true);
+ CancelButton.Click += (_, _) => Close(false);
+ }
+}