Этап 8

This commit is contained in:
2026-03-29 16:51:43 +03:00
parent 25e30416a3
commit c3961fcba7
5 changed files with 452 additions and 34 deletions

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Avalonia;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -17,6 +19,7 @@ public partial class EditorViewModel : ViewModelBase
private readonly IPaletteService _paletteService = new PaletteService();
private readonly IDrawingService _drawingService = new DrawingService();
private readonly IFloodFillService _floodFillService = new FloodFillService();
private readonly IFragmentService _fragmentService = new FragmentService();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasContainer))]
@@ -44,17 +47,11 @@ public partial class EditorViewModel : ViewModelBase
[ObservableProperty]
private int _brushRadius = 1;
/// <summary>
/// Pixel coordinates of current brush/eraser preview center, or null if cursor is outside image.
/// </summary>
[ObservableProperty]
private (int X, int Y)? _previewCenter;
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
{
get => _previewColor;
@@ -98,10 +95,7 @@ public partial class EditorViewModel : ViewModelBase
Container = container;
FilePath = path;
Documents.Clear();
foreach (var doc in container.Documents)
Documents.Add(doc);
SyncDocumentsList();
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
}
@@ -119,8 +113,17 @@ public partial class EditorViewModel : ViewModelBase
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)
{
UnsubscribeLayerVisibility();
Layers.Clear();
if (doc is not null)
{
@@ -132,6 +135,178 @@ public partial class EditorViewModel : ViewModelBase
{
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();
}
@@ -139,9 +314,6 @@ public partial class EditorViewModel : ViewModelBase
#region Drawing
/// <summary>
/// Called by PixelCanvas on left-click at the given image pixel coordinate.
/// </summary>
public void OnToolDown(int px, int py)
{
if (Container is null || ActiveDocument is null || ActiveLayer is null)
@@ -173,19 +345,12 @@ public partial class EditorViewModel : ViewModelBase
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)
{
if (ActiveTool == ToolType.Fill) return;
OnToolDown(px, py);
}
/// <summary>
/// Returns brush mask pixels for tool preview overlay.
/// </summary>
public List<(int X, int Y)>? GetPreviewMask()
{
if (PreviewCenter is null || Container is null)
@@ -208,6 +373,28 @@ public partial class EditorViewModel : ViewModelBase
#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
public void RefreshCanvas()
@@ -267,4 +454,5 @@ public partial class EditorViewModel : ViewModelBase
public ICompositor Compositor => _compositor;
public IPaletteService PaletteService => _paletteService;
public IDrawingService DrawingService => _drawingService;
public IFragmentService FragmentService => _fragmentService;
}