Этап 9

This commit is contained in:
2026-03-29 17:25:33 +03:00
parent c3961fcba7
commit 3a61e0a07d
14 changed files with 622 additions and 20 deletions

View 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);
}

View 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);
}
}

View 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);
}

View File

@@ -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)
_viewportInitialized = false;
{
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;
}
}
}
}

View File

@@ -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.

View File

@@ -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();

View File

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

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

View 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);
}
}

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

View 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);
}
}

View File

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

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

View 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);
}
}