Этап 9
This commit is contained in:
18
Minint.Core/Services/IImageEffectsService.cs
Normal file
18
Minint.Core/Services/IImageEffectsService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Minint.Core.Models;
|
||||
|
||||
namespace Minint.Core.Services;
|
||||
|
||||
public interface IImageEffectsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Adjusts contrast of the document by transforming its palette colors.
|
||||
/// <paramref name="factor"/> of 0 = all gray, 1 = no change, >1 = increased contrast.
|
||||
/// </summary>
|
||||
void ApplyContrast(MinintDocument doc, double factor);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the document to grayscale by transforming its palette colors
|
||||
/// using the luminance formula: 0.299R + 0.587G + 0.114B.
|
||||
/// </summary>
|
||||
void ApplyGrayscale(MinintDocument doc);
|
||||
}
|
||||
38
Minint.Core/Services/Impl/ImageEffectsService.cs
Normal file
38
Minint.Core/Services/Impl/ImageEffectsService.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
113
Minint.Core/Services/Impl/PatternGenerator.cs
Normal file
113
Minint.Core/Services/Impl/PatternGenerator.cs
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates the pixel coordinate under the cursor after a viewport change.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
var bmp = change.GetNewValue<WriteableBitmap?>();
|
||||
int w = bmp?.PixelSize.Width ?? 0;
|
||||
int h = bmp?.PixelSize.Height ?? 0;
|
||||
if (w != _lastBitmapWidth || h != _lastBitmapHeight)
|
||||
{
|
||||
_lastBitmapWidth = w;
|
||||
_lastBitmapHeight = h;
|
||||
_viewportInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ sealed class Program
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
.LogToTrace()
|
||||
.With(new X11PlatformOptions { OverlayPopups = true });
|
||||
|
||||
// TODO: temporary tests — remove after verification stages.
|
||||
|
||||
|
||||
@@ -113,6 +113,12 @@ public partial class EditorViewModel : ViewModelBase
|
||||
SyncLayersAndCanvas(doc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-syncs the Documents observable collection after an external modification
|
||||
/// to Container.Documents (e.g. pattern generation adding a document).
|
||||
/// </summary>
|
||||
public void SyncAfterExternalChange() => SyncDocumentsList();
|
||||
|
||||
private void SyncDocumentsList()
|
||||
{
|
||||
Documents.Clear();
|
||||
|
||||
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// Set by the view so that file dialogs can use the correct parent window.
|
||||
/// </summary>
|
||||
public TopLevel? Owner { get; set; }
|
||||
|
||||
#region File commands
|
||||
|
||||
[RelayCommand]
|
||||
private void NewFile()
|
||||
{
|
||||
@@ -72,14 +77,10 @@ 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]
|
||||
private async Task SaveFileAsAsync()
|
||||
@@ -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<bool?>(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<bool?>(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<bool?>(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
|
||||
}
|
||||
|
||||
20
Minint/Views/ContrastDialog.axaml
Normal file
20
Minint/Views/ContrastDialog.axaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Minint.Views.ContrastDialog"
|
||||
Title="Adjust Contrast"
|
||||
Width="320"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
SizeToContent="Height">
|
||||
<StackPanel Margin="16" Spacing="10">
|
||||
<TextBlock Text="Contrast factor (0 = gray, 1 = no change, >1 = more contrast):"/>
|
||||
<Slider x:Name="FactorSlider" Minimum="0" Maximum="3" Value="1"
|
||||
TickFrequency="0.1" IsSnapToTickEnabled="True"/>
|
||||
<TextBlock x:Name="FactorLabel" Text="1.0" HorizontalAlignment="Center"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,8,0,0">
|
||||
<Button Content="Apply" x:Name="OkButton" IsDefault="True" Padding="16,6"/>
|
||||
<Button Content="Cancel" x:Name="CancelButton" IsCancel="True" Padding="16,6"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
24
Minint/Views/ContrastDialog.axaml.cs
Normal file
24
Minint/Views/ContrastDialog.axaml.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
51
Minint/Views/CopyFragmentDialog.axaml
Normal file
51
Minint/Views/CopyFragmentDialog.axaml
Normal file
@@ -0,0 +1,51 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Minint.Views.CopyFragmentDialog"
|
||||
Title="Copy Fragment"
|
||||
Width="380" Height="420"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
SizeToContent="Height">
|
||||
<StackPanel Margin="16" Spacing="8">
|
||||
<TextBlock Text="Source" FontWeight="SemiBold"/>
|
||||
<Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowSpacing="4" ColumnSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Document:" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" x:Name="SrcDocCombo"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Layer:" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="SrcLayerCombo"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="X:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" x:Name="SrcX" Value="0" Minimum="0" FormatString="0"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Y:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" x:Name="SrcY" Value="0" Minimum="0" FormatString="0"/>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto" RowSpacing="4" ColumnSpacing="8" Margin="0,4,0,0">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Width:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="0" Grid.Column="1" x:Name="RegionW" Value="16" Minimum="1" FormatString="0"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Height:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" x:Name="RegionH" Value="16" Minimum="1" FormatString="0"/>
|
||||
</Grid>
|
||||
|
||||
<Separator Margin="0,8"/>
|
||||
<TextBlock Text="Destination" FontWeight="SemiBold"/>
|
||||
<Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="4" ColumnSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Document:" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" x:Name="DstDocCombo"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Layer:" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="DstLayerCombo"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="X:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" x:Name="DstX" Value="0" Minimum="0" FormatString="0"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Y:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" x:Name="DstY" Value="0" Minimum="0" FormatString="0"/>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,12,0,0">
|
||||
<Button Content="Copy" x:Name="OkButton" IsDefault="True" Padding="16,6"/>
|
||||
<Button Content="Cancel" x:Name="CancelButton" IsCancel="True" Padding="16,6"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
91
Minint/Views/CopyFragmentDialog.axaml.cs
Normal file
91
Minint/Views/CopyFragmentDialog.axaml.cs
Normal file
@@ -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<MinintDocument> _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<MinintDocument> 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);
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel/>
|
||||
@@ -25,6 +26,19 @@
|
||||
<MenuItem Header="_Save" Command="{Binding SaveFileCommand}" HotKey="Ctrl+S"/>
|
||||
<MenuItem Header="Save _As…" Command="{Binding SaveFileAsCommand}" HotKey="Ctrl+Shift+S"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Edit">
|
||||
<MenuItem Header="Copy _Fragment…" Command="{Binding CopyFragmentCommand}"
|
||||
ToolTip.Tip="Copy a rectangular region between documents"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Image">
|
||||
<MenuItem Header="Adjust _Contrast…" Command="{Binding ApplyContrastCommand}"
|
||||
ToolTip.Tip="Adjust contrast of the active document's palette"/>
|
||||
<MenuItem Header="Convert to _Grayscale" Command="{Binding ApplyGrayscaleCommand}"
|
||||
ToolTip.Tip="Convert active document to grayscale"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Generate _Pattern…" Command="{Binding GeneratePatternCommand}"
|
||||
ToolTip.Tip="Generate a new document with a parametric pattern"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_View">
|
||||
<MenuItem Header="Pixel _Grid" ToggleType="CheckBox"
|
||||
IsChecked="{Binding Editor.ShowGrid}" HotKey="Ctrl+G"/>
|
||||
@@ -83,13 +97,13 @@
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="Documents" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="2" Margin="0,4,0,0">
|
||||
<Button Content="+" ToolTip.Tip="Add document"
|
||||
<Button Content="+" ToolTip.Tip="Add a new document (frame)"
|
||||
Command="{Binding Editor.AddDocumentCommand}" Padding="6,2"/>
|
||||
<Button Content="−" ToolTip.Tip="Remove document"
|
||||
<Button Content="−" ToolTip.Tip="Remove selected document"
|
||||
Command="{Binding Editor.RemoveDocumentCommand}" Padding="6,2"/>
|
||||
<Button Content="▲" ToolTip.Tip="Move up"
|
||||
<Button Content="▲" ToolTip.Tip="Move document up in the list"
|
||||
Command="{Binding Editor.MoveDocumentUpCommand}" Padding="6,2"/>
|
||||
<Button Content="▼" ToolTip.Tip="Move down"
|
||||
<Button Content="▼" ToolTip.Tip="Move document down in the list"
|
||||
Command="{Binding Editor.MoveDocumentDownCommand}" Padding="6,2"/>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding Editor.Documents}"
|
||||
@@ -123,15 +137,15 @@
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="Layers" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="2" Margin="0,4,0,0">
|
||||
<Button Content="+" ToolTip.Tip="Add layer"
|
||||
<Button Content="+" ToolTip.Tip="Add a new empty layer"
|
||||
Command="{Binding Editor.AddLayerCommand}" Padding="6,2"/>
|
||||
<Button Content="−" ToolTip.Tip="Remove layer"
|
||||
<Button Content="−" ToolTip.Tip="Remove selected layer"
|
||||
Command="{Binding Editor.RemoveLayerCommand}" Padding="6,2"/>
|
||||
<Button Content="▲" ToolTip.Tip="Move up"
|
||||
<Button Content="▲" ToolTip.Tip="Move layer up (draw later, appears on top)"
|
||||
Command="{Binding Editor.MoveLayerUpCommand}" Padding="6,2"/>
|
||||
<Button Content="▼" ToolTip.Tip="Move down"
|
||||
<Button Content="▼" ToolTip.Tip="Move layer down (draw earlier, appears below)"
|
||||
Command="{Binding Editor.MoveLayerDownCommand}" Padding="6,2"/>
|
||||
<Button Content="⧉" ToolTip.Tip="Duplicate layer"
|
||||
<Button Content="⧉" ToolTip.Tip="Duplicate selected layer with all pixels"
|
||||
Command="{Binding Editor.DuplicateLayerCommand}" Padding="6,2"/>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding Editor.Layers}"
|
||||
|
||||
36
Minint/Views/PatternDialog.axaml
Normal file
36
Minint/Views/PatternDialog.axaml
Normal file
@@ -0,0 +1,36 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Minint.Views.PatternDialog"
|
||||
Title="Generate Pattern"
|
||||
Width="360"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
SizeToContent="Height">
|
||||
<StackPanel Margin="16" Spacing="8">
|
||||
<Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" RowSpacing="6" ColumnSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Pattern:" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" x:Name="PatternCombo" HorizontalAlignment="Stretch"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Color 1:" VerticalAlignment="Center"/>
|
||||
<ColorPicker Grid.Row="1" Grid.Column="1" x:Name="Color1Picker" IsAlphaVisible="False"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Color 2:" VerticalAlignment="Center"/>
|
||||
<ColorPicker Grid.Row="2" Grid.Column="1" x:Name="Color2Picker" IsAlphaVisible="False"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Param 1:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" x:Name="Param1"
|
||||
Value="8" Minimum="1" Maximum="256" FormatString="0"
|
||||
ToolTip.Tip="Cell/stripe/ring size"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Param 2:" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Grid.Row="4" Grid.Column="1" x:Name="Param2"
|
||||
Value="8" Minimum="1" Maximum="256" FormatString="0"
|
||||
ToolTip.Tip="Tile height (for Tile pattern)"/>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,12,0,0">
|
||||
<Button Content="Generate" x:Name="OkButton" IsDefault="True" Padding="16,6"/>
|
||||
<Button Content="Cancel" x:Name="CancelButton" IsCancel="True" Padding="16,6"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
50
Minint/Views/PatternDialog.axaml.cs
Normal file
50
Minint/Views/PatternDialog.axaml.cs
Normal file
@@ -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<PatternType>().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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user