Этап 8
This commit is contained in:
@@ -1,19 +1,39 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Minint.Core.Models;
|
namespace Minint.Core.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A single raster layer. Pixels are indices into the parent document's palette.
|
/// A single raster layer. Pixels are indices into the parent document's palette.
|
||||||
/// Array layout is row-major: Pixels[y * width + x].
|
/// Array layout is row-major: Pixels[y * width + x].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MinintLayer
|
public sealed class MinintLayer : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
private string _name;
|
||||||
public bool IsVisible { get; set; }
|
private bool _isVisible;
|
||||||
|
private byte _opacity;
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set { if (_name != value) { _name = value; Notify(); } }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsVisible
|
||||||
|
{
|
||||||
|
get => _isVisible;
|
||||||
|
set { if (_isVisible != value) { _isVisible = value; Notify(); } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-layer opacity (0 = fully transparent, 255 = fully opaque).
|
/// Per-layer opacity (0 = fully transparent, 255 = fully opaque).
|
||||||
/// Used during compositing: effective alpha = paletteColor.A * Opacity / 255.
|
/// Used during compositing: effective alpha = paletteColor.A * Opacity / 255.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte Opacity { get; set; }
|
public byte Opacity
|
||||||
|
{
|
||||||
|
get => _opacity;
|
||||||
|
set { if (_opacity != value) { _opacity = value; Notify(); } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Palette indices, length must equal container Width * Height.
|
/// Palette indices, length must equal container Width * Height.
|
||||||
@@ -23,9 +43,9 @@ public sealed class MinintLayer
|
|||||||
|
|
||||||
public MinintLayer(string name, int pixelCount)
|
public MinintLayer(string name, int pixelCount)
|
||||||
{
|
{
|
||||||
Name = name;
|
_name = name;
|
||||||
IsVisible = true;
|
_isVisible = true;
|
||||||
Opacity = 255;
|
_opacity = 255;
|
||||||
Pixels = new int[pixelCount];
|
Pixels = new int[pixelCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +54,13 @@ public sealed class MinintLayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MinintLayer(string name, bool isVisible, byte opacity, int[] pixels)
|
public MinintLayer(string name, bool isVisible, byte opacity, int[] pixels)
|
||||||
{
|
{
|
||||||
Name = name;
|
_name = name;
|
||||||
IsVisible = isVisible;
|
_isVisible = isVisible;
|
||||||
Opacity = opacity;
|
_opacity = opacity;
|
||||||
Pixels = pixels;
|
Pixels = pixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
private void Notify([CallerMemberName] string? prop = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||||
}
|
}
|
||||||
|
|||||||
49
Minint.Core/Services/Impl/FragmentService.cs
Normal file
49
Minint.Core/Services/Impl/FragmentService.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using Minint.Core.Models;
|
||||||
|
|
||||||
|
namespace Minint.Core.Services.Impl;
|
||||||
|
|
||||||
|
public sealed class FragmentService : IFragmentService
|
||||||
|
{
|
||||||
|
public void CopyFragment(
|
||||||
|
MinintDocument srcDoc, int srcLayerIndex,
|
||||||
|
int srcX, int srcY, int regionWidth, int regionHeight,
|
||||||
|
MinintDocument dstDoc, int dstLayerIndex,
|
||||||
|
int dstX, int dstY,
|
||||||
|
int containerWidth, int containerHeight)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(srcLayerIndex);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(dstLayerIndex);
|
||||||
|
if (srcLayerIndex >= srcDoc.Layers.Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(srcLayerIndex));
|
||||||
|
if (dstLayerIndex >= dstDoc.Layers.Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(dstLayerIndex));
|
||||||
|
|
||||||
|
var srcLayer = srcDoc.Layers[srcLayerIndex];
|
||||||
|
var dstLayer = dstDoc.Layers[dstLayerIndex];
|
||||||
|
|
||||||
|
int clippedSrcX = Math.Max(srcX, 0);
|
||||||
|
int clippedSrcY = Math.Max(srcY, 0);
|
||||||
|
int clippedEndX = Math.Min(srcX + regionWidth, containerWidth);
|
||||||
|
int clippedEndY = Math.Min(srcY + regionHeight, containerHeight);
|
||||||
|
|
||||||
|
for (int sy = clippedSrcY; sy < clippedEndY; sy++)
|
||||||
|
{
|
||||||
|
int dy = dstY + (sy - srcY);
|
||||||
|
if (dy < 0 || dy >= containerHeight) continue;
|
||||||
|
|
||||||
|
for (int sx = clippedSrcX; sx < clippedEndX; sx++)
|
||||||
|
{
|
||||||
|
int dx = dstX + (sx - srcX);
|
||||||
|
if (dx < 0 || dx >= containerWidth) continue;
|
||||||
|
|
||||||
|
int srcIdx = srcLayer.Pixels[sy * containerWidth + sx];
|
||||||
|
if (srcIdx == 0) continue; // skip transparent
|
||||||
|
|
||||||
|
RgbaColor color = srcDoc.Palette[srcIdx];
|
||||||
|
int dstIdx = dstDoc.EnsureColorCached(color);
|
||||||
|
dstLayer.Pixels[dy * containerWidth + dx] = dstIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
Minint/Controls/EditableTextBlock.cs
Normal file
129
Minint/Controls/EditableTextBlock.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace Minint.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a TextBlock by default; switches to an inline TextBox on double-click.
|
||||||
|
/// Commits on Enter or focus loss, cancels on Escape.
|
||||||
|
/// </summary>
|
||||||
|
public class EditableTextBlock : Control
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<string> TextProperty =
|
||||||
|
AvaloniaProperty.Register<EditableTextBlock, string>(nameof(Text), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => GetValue(TextProperty);
|
||||||
|
set => SetValue(TextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly TextBlock _display;
|
||||||
|
private readonly TextBox _editor;
|
||||||
|
private bool _isEditing;
|
||||||
|
|
||||||
|
public EditableTextBlock()
|
||||||
|
{
|
||||||
|
_display = new TextBlock
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
};
|
||||||
|
|
||||||
|
_editor = new TextBox
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Padding = new Thickness(2, 0),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
MinWidth = 40,
|
||||||
|
IsVisible = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
LogicalChildren.Add(_display);
|
||||||
|
LogicalChildren.Add(_editor);
|
||||||
|
VisualChildren.Add(_display);
|
||||||
|
VisualChildren.Add(_editor);
|
||||||
|
|
||||||
|
_display.Bind(TextBlock.TextProperty, this.GetObservable(TextProperty).ToBinding());
|
||||||
|
_editor.Bind(TextBox.TextProperty, this.GetObservable(TextProperty).ToBinding());
|
||||||
|
|
||||||
|
_editor.KeyDown += OnEditorKeyDown;
|
||||||
|
_editor.LostFocus += OnEditorLostFocus;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerPressed(e);
|
||||||
|
if (e.ClickCount == 2 && !_isEditing)
|
||||||
|
{
|
||||||
|
BeginEdit();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginEdit()
|
||||||
|
{
|
||||||
|
_isEditing = true;
|
||||||
|
_editor.Text = Text;
|
||||||
|
_display.IsVisible = false;
|
||||||
|
_editor.IsVisible = true;
|
||||||
|
_editor.Focus();
|
||||||
|
_editor.SelectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CommitEdit()
|
||||||
|
{
|
||||||
|
if (!_isEditing) return;
|
||||||
|
_isEditing = false;
|
||||||
|
Text = _editor.Text ?? string.Empty;
|
||||||
|
_editor.IsVisible = false;
|
||||||
|
_display.IsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelEdit()
|
||||||
|
{
|
||||||
|
if (!_isEditing) return;
|
||||||
|
_isEditing = false;
|
||||||
|
_editor.Text = Text;
|
||||||
|
_editor.IsVisible = false;
|
||||||
|
_display.IsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEditorKeyDown(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Enter)
|
||||||
|
{
|
||||||
|
CommitEdit();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.Escape)
|
||||||
|
{
|
||||||
|
CancelEdit();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEditorLostFocus(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CommitEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
_display.Measure(availableSize);
|
||||||
|
_editor.Measure(availableSize);
|
||||||
|
return _isEditing ? _editor.DesiredSize : _display.DesiredSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
|
{
|
||||||
|
var rect = new Rect(finalSize);
|
||||||
|
_display.Arrange(rect);
|
||||||
|
_editor.Arrange(rect);
|
||||||
|
return finalSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -17,6 +19,7 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
private readonly IPaletteService _paletteService = new PaletteService();
|
private readonly IPaletteService _paletteService = new PaletteService();
|
||||||
private readonly IDrawingService _drawingService = new DrawingService();
|
private readonly IDrawingService _drawingService = new DrawingService();
|
||||||
private readonly IFloodFillService _floodFillService = new FloodFillService();
|
private readonly IFloodFillService _floodFillService = new FloodFillService();
|
||||||
|
private readonly IFragmentService _fragmentService = new FragmentService();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
||||||
@@ -44,17 +47,11 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private int _brushRadius = 1;
|
private int _brushRadius = 1;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pixel coordinates of current brush/eraser preview center, or null if cursor is outside image.
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private (int X, int Y)? _previewCenter;
|
private (int X, int Y)? _previewCenter;
|
||||||
|
|
||||||
private Avalonia.Media.Color _previewColor = Avalonia.Media.Color.FromArgb(255, 0, 0, 0);
|
private Avalonia.Media.Color _previewColor = Avalonia.Media.Color.FromArgb(255, 0, 0, 0);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Avalonia Color bound two-way to the ColorPicker in the toolbar.
|
|
||||||
/// </summary>
|
|
||||||
public Avalonia.Media.Color PreviewColor
|
public Avalonia.Media.Color PreviewColor
|
||||||
{
|
{
|
||||||
get => _previewColor;
|
get => _previewColor;
|
||||||
@@ -98,10 +95,7 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
Container = container;
|
Container = container;
|
||||||
FilePath = path;
|
FilePath = path;
|
||||||
|
|
||||||
Documents.Clear();
|
SyncDocumentsList();
|
||||||
foreach (var doc in container.Documents)
|
|
||||||
Documents.Add(doc);
|
|
||||||
|
|
||||||
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
|
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +113,17 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
SyncLayersAndCanvas(doc);
|
SyncLayersAndCanvas(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SyncDocumentsList()
|
||||||
|
{
|
||||||
|
Documents.Clear();
|
||||||
|
if (Container is null) return;
|
||||||
|
foreach (var doc in Container.Documents)
|
||||||
|
Documents.Add(doc);
|
||||||
|
}
|
||||||
|
|
||||||
private void SyncLayersAndCanvas(MinintDocument? doc)
|
private void SyncLayersAndCanvas(MinintDocument? doc)
|
||||||
{
|
{
|
||||||
|
UnsubscribeLayerVisibility();
|
||||||
Layers.Clear();
|
Layers.Clear();
|
||||||
if (doc is not null)
|
if (doc is not null)
|
||||||
{
|
{
|
||||||
@@ -132,6 +135,178 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
ActiveLayer = null;
|
ActiveLayer = null;
|
||||||
}
|
}
|
||||||
|
SubscribeLayerVisibility();
|
||||||
|
RefreshCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Layer visibility change tracking
|
||||||
|
|
||||||
|
private void SubscribeLayerVisibility()
|
||||||
|
{
|
||||||
|
foreach (var layer in Layers)
|
||||||
|
{
|
||||||
|
if (layer is INotifyPropertyChanged npc)
|
||||||
|
npc.PropertyChanged += OnLayerPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnsubscribeLayerVisibility()
|
||||||
|
{
|
||||||
|
foreach (var layer in Layers)
|
||||||
|
{
|
||||||
|
if (layer is INotifyPropertyChanged npc)
|
||||||
|
npc.PropertyChanged -= OnLayerPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName is nameof(MinintLayer.IsVisible) or nameof(MinintLayer.Opacity))
|
||||||
|
RefreshCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Document commands
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddDocument()
|
||||||
|
{
|
||||||
|
if (Container is null) return;
|
||||||
|
int num = Container.Documents.Count + 1;
|
||||||
|
var doc = Container.AddNewDocument($"Document {num}");
|
||||||
|
Documents.Add(doc);
|
||||||
|
SelectDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveDocument()
|
||||||
|
{
|
||||||
|
if (Container is null || ActiveDocument is null) return;
|
||||||
|
if (Container.Documents.Count <= 1) return; // keep at least one
|
||||||
|
|
||||||
|
var doc = ActiveDocument;
|
||||||
|
int idx = Container.Documents.IndexOf(doc);
|
||||||
|
Container.Documents.Remove(doc);
|
||||||
|
Documents.Remove(doc);
|
||||||
|
|
||||||
|
int newIdx = Math.Min(idx, Container.Documents.Count - 1);
|
||||||
|
SelectDocument(newIdx >= 0 ? Container.Documents[newIdx] : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RenameDocument()
|
||||||
|
{
|
||||||
|
// Triggered via UI text edit — the Name property is directly editable via TextBox
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void MoveDocumentUp()
|
||||||
|
{
|
||||||
|
if (Container is null || ActiveDocument is null) return;
|
||||||
|
int idx = Container.Documents.IndexOf(ActiveDocument);
|
||||||
|
if (idx <= 0) return;
|
||||||
|
(Container.Documents[idx], Container.Documents[idx - 1]) = (Container.Documents[idx - 1], Container.Documents[idx]);
|
||||||
|
SyncDocumentsList();
|
||||||
|
_suppressDocumentSync = true;
|
||||||
|
ActiveDocument = Container.Documents[idx - 1];
|
||||||
|
_suppressDocumentSync = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void MoveDocumentDown()
|
||||||
|
{
|
||||||
|
if (Container is null || ActiveDocument is null) return;
|
||||||
|
int idx = Container.Documents.IndexOf(ActiveDocument);
|
||||||
|
if (idx < 0 || idx >= Container.Documents.Count - 1) return;
|
||||||
|
(Container.Documents[idx], Container.Documents[idx + 1]) = (Container.Documents[idx + 1], Container.Documents[idx]);
|
||||||
|
SyncDocumentsList();
|
||||||
|
_suppressDocumentSync = true;
|
||||||
|
ActiveDocument = Container.Documents[idx + 1];
|
||||||
|
_suppressDocumentSync = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Layer commands
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddLayer()
|
||||||
|
{
|
||||||
|
if (Container is null || ActiveDocument is null) return;
|
||||||
|
int num = ActiveDocument.Layers.Count + 1;
|
||||||
|
var layer = new MinintLayer($"Layer {num}", Container.PixelCount);
|
||||||
|
ActiveDocument.Layers.Add(layer);
|
||||||
|
Layers.Add(layer);
|
||||||
|
ActiveLayer = layer;
|
||||||
|
SubscribeLayerVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveLayer()
|
||||||
|
{
|
||||||
|
if (ActiveDocument is null || ActiveLayer is null) return;
|
||||||
|
if (ActiveDocument.Layers.Count <= 1) return; // keep at least one
|
||||||
|
|
||||||
|
UnsubscribeLayerVisibility();
|
||||||
|
var layer = ActiveLayer;
|
||||||
|
int idx = ActiveDocument.Layers.IndexOf(layer);
|
||||||
|
ActiveDocument.Layers.Remove(layer);
|
||||||
|
Layers.Remove(layer);
|
||||||
|
|
||||||
|
int newIdx = Math.Min(idx, ActiveDocument.Layers.Count - 1);
|
||||||
|
ActiveLayer = newIdx >= 0 ? ActiveDocument.Layers[newIdx] : null;
|
||||||
|
SubscribeLayerVisibility();
|
||||||
|
RefreshCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void MoveLayerUp()
|
||||||
|
{
|
||||||
|
if (ActiveDocument is null || ActiveLayer is null) return;
|
||||||
|
int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
|
||||||
|
if (idx <= 0) return;
|
||||||
|
|
||||||
|
UnsubscribeLayerVisibility();
|
||||||
|
var layer = ActiveLayer;
|
||||||
|
(ActiveDocument.Layers[idx], ActiveDocument.Layers[idx - 1]) = (ActiveDocument.Layers[idx - 1], ActiveDocument.Layers[idx]);
|
||||||
|
Layers.Move(idx, idx - 1);
|
||||||
|
ActiveLayer = layer;
|
||||||
|
SubscribeLayerVisibility();
|
||||||
|
RefreshCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void MoveLayerDown()
|
||||||
|
{
|
||||||
|
if (ActiveDocument is null || ActiveLayer is null) return;
|
||||||
|
int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
|
||||||
|
if (idx < 0 || idx >= ActiveDocument.Layers.Count - 1) return;
|
||||||
|
|
||||||
|
UnsubscribeLayerVisibility();
|
||||||
|
var layer = ActiveLayer;
|
||||||
|
(ActiveDocument.Layers[idx], ActiveDocument.Layers[idx + 1]) = (ActiveDocument.Layers[idx + 1], ActiveDocument.Layers[idx]);
|
||||||
|
Layers.Move(idx, idx + 1);
|
||||||
|
ActiveLayer = layer;
|
||||||
|
SubscribeLayerVisibility();
|
||||||
|
RefreshCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void DuplicateLayer()
|
||||||
|
{
|
||||||
|
if (Container is null || ActiveDocument is null || ActiveLayer is null) return;
|
||||||
|
var src = ActiveLayer;
|
||||||
|
var dup = new MinintLayer(src.Name + " copy", src.IsVisible, src.Opacity, (int[])src.Pixels.Clone());
|
||||||
|
int idx = ActiveDocument.Layers.IndexOf(src) + 1;
|
||||||
|
ActiveDocument.Layers.Insert(idx, dup);
|
||||||
|
|
||||||
|
UnsubscribeLayerVisibility();
|
||||||
|
Layers.Insert(idx, dup);
|
||||||
|
ActiveLayer = dup;
|
||||||
|
SubscribeLayerVisibility();
|
||||||
RefreshCanvas();
|
RefreshCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,9 +314,6 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
|
|
||||||
#region Drawing
|
#region Drawing
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by PixelCanvas on left-click at the given image pixel coordinate.
|
|
||||||
/// </summary>
|
|
||||||
public void OnToolDown(int px, int py)
|
public void OnToolDown(int px, int py)
|
||||||
{
|
{
|
||||||
if (Container is null || ActiveDocument is null || ActiveLayer is null)
|
if (Container is null || ActiveDocument is null || ActiveLayer is null)
|
||||||
@@ -173,19 +345,12 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
RefreshCanvas();
|
RefreshCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by PixelCanvas on left-drag at the given image pixel coordinate.
|
|
||||||
/// Same as OnToolDown for brush/eraser, no-op for fill.
|
|
||||||
/// </summary>
|
|
||||||
public void OnToolDrag(int px, int py)
|
public void OnToolDrag(int px, int py)
|
||||||
{
|
{
|
||||||
if (ActiveTool == ToolType.Fill) return;
|
if (ActiveTool == ToolType.Fill) return;
|
||||||
OnToolDown(px, py);
|
OnToolDown(px, py);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns brush mask pixels for tool preview overlay.
|
|
||||||
/// </summary>
|
|
||||||
public List<(int X, int Y)>? GetPreviewMask()
|
public List<(int X, int Y)>? GetPreviewMask()
|
||||||
{
|
{
|
||||||
if (PreviewCenter is null || Container is null)
|
if (PreviewCenter is null || Container is null)
|
||||||
@@ -208,6 +373,28 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Fragment copy (A4)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies a rectangular fragment from <paramref name="srcDoc"/> active layer
|
||||||
|
/// to <paramref name="dstDoc"/> active layer.
|
||||||
|
/// </summary>
|
||||||
|
public void CopyFragment(
|
||||||
|
MinintDocument srcDoc, int srcLayerIndex,
|
||||||
|
int srcX, int srcY, int regionW, int regionH,
|
||||||
|
MinintDocument dstDoc, int dstLayerIndex,
|
||||||
|
int dstX, int dstY)
|
||||||
|
{
|
||||||
|
if (Container is null) return;
|
||||||
|
_fragmentService.CopyFragment(
|
||||||
|
srcDoc, srcLayerIndex, srcX, srcY, regionW, regionH,
|
||||||
|
dstDoc, dstLayerIndex, dstX, dstY,
|
||||||
|
Container.Width, Container.Height);
|
||||||
|
RefreshCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Canvas rendering
|
#region Canvas rendering
|
||||||
|
|
||||||
public void RefreshCanvas()
|
public void RefreshCanvas()
|
||||||
@@ -267,4 +454,5 @@ public partial class EditorViewModel : ViewModelBase
|
|||||||
public ICompositor Compositor => _compositor;
|
public ICompositor Compositor => _compositor;
|
||||||
public IPaletteService PaletteService => _paletteService;
|
public IPaletteService PaletteService => _paletteService;
|
||||||
public IDrawingService DrawingService => _drawingService;
|
public IDrawingService DrawingService => _drawingService;
|
||||||
|
public IFragmentService FragmentService => _fragmentService;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,15 @@
|
|||||||
BorderThickness="0,0,0,1" Padding="6,4">
|
BorderThickness="0,0,0,1" Padding="6,4">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
<RadioButton GroupName="Tool" Content="Brush"
|
<RadioButton GroupName="Tool" Content="Brush"
|
||||||
|
ToolTip.Tip="Brush tool — draw with selected color"
|
||||||
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsBrush}}"
|
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsBrush}}"
|
||||||
Command="{Binding Editor.SelectBrushCommand}"/>
|
Command="{Binding Editor.SelectBrushCommand}"/>
|
||||||
<RadioButton GroupName="Tool" Content="Eraser"
|
<RadioButton GroupName="Tool" Content="Eraser"
|
||||||
|
ToolTip.Tip="Eraser tool — erase to transparent"
|
||||||
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsEraser}}"
|
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsEraser}}"
|
||||||
Command="{Binding Editor.SelectEraserCommand}"/>
|
Command="{Binding Editor.SelectEraserCommand}"/>
|
||||||
<RadioButton GroupName="Tool" Content="Fill"
|
<RadioButton GroupName="Tool" Content="Fill"
|
||||||
|
ToolTip.Tip="Fill tool — flood fill with selected color"
|
||||||
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsFill}}"
|
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsFill}}"
|
||||||
Command="{Binding Editor.SelectFillCommand}"/>
|
Command="{Binding Editor.SelectFillCommand}"/>
|
||||||
|
|
||||||
@@ -72,19 +75,29 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Main content: left panel, canvas, right panel -->
|
<!-- Main content: left panel, canvas, right panel -->
|
||||||
<Grid ColumnDefinitions="180,*,180">
|
<Grid ColumnDefinitions="200,*,200">
|
||||||
|
|
||||||
<!-- Left panel: documents -->
|
<!-- Left panel: documents -->
|
||||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
||||||
BorderThickness="0,0,1,0" Padding="4">
|
BorderThickness="0,0,1,0" Padding="4">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Documents" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
<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"
|
||||||
|
Command="{Binding Editor.AddDocumentCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="−" ToolTip.Tip="Remove document"
|
||||||
|
Command="{Binding Editor.RemoveDocumentCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="▲" ToolTip.Tip="Move up"
|
||||||
|
Command="{Binding Editor.MoveDocumentUpCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="▼" ToolTip.Tip="Move down"
|
||||||
|
Command="{Binding Editor.MoveDocumentDownCommand}" Padding="6,2"/>
|
||||||
|
</StackPanel>
|
||||||
<ListBox ItemsSource="{Binding Editor.Documents}"
|
<ListBox ItemsSource="{Binding Editor.Documents}"
|
||||||
SelectedItem="{Binding Editor.ActiveDocument}"
|
SelectedItem="{Binding Editor.ActiveDocument}"
|
||||||
SelectionMode="Single">
|
SelectionMode="Single">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Name}"/>
|
<controls:EditableTextBlock Text="{Binding Name}"/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
@@ -109,14 +122,29 @@
|
|||||||
BorderThickness="1,0,0,0" Padding="4">
|
BorderThickness="1,0,0,0" Padding="4">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Layers" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
<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"
|
||||||
|
Command="{Binding Editor.AddLayerCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="−" ToolTip.Tip="Remove layer"
|
||||||
|
Command="{Binding Editor.RemoveLayerCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="▲" ToolTip.Tip="Move up"
|
||||||
|
Command="{Binding Editor.MoveLayerUpCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="▼" ToolTip.Tip="Move down"
|
||||||
|
Command="{Binding Editor.MoveLayerDownCommand}" Padding="6,2"/>
|
||||||
|
<Button Content="⧉" ToolTip.Tip="Duplicate layer"
|
||||||
|
Command="{Binding Editor.DuplicateLayerCommand}" Padding="6,2"/>
|
||||||
|
</StackPanel>
|
||||||
<ListBox ItemsSource="{Binding Editor.Layers}"
|
<ListBox ItemsSource="{Binding Editor.Layers}"
|
||||||
SelectedItem="{Binding Editor.ActiveLayer}"
|
SelectedItem="{Binding Editor.ActiveLayer}"
|
||||||
SelectionMode="Single">
|
SelectionMode="Single">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<CheckBox IsChecked="{Binding IsVisible}" MinWidth="0"/>
|
<CheckBox IsChecked="{Binding IsVisible}" MinWidth="0"
|
||||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="Toggle layer visibility"/>
|
||||||
|
<controls:EditableTextBlock Text="{Binding Name}"
|
||||||
|
VerticalAlignment="Center" MinWidth="60"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
|
|||||||
Reference in New Issue
Block a user