Этап 7
This commit is contained in:
@@ -1,163 +1,270 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Minint.Core.Models;
|
||||
using Minint.Core.Services;
|
||||
using Minint.Core.Services.Impl;
|
||||
|
||||
namespace Minint.ViewModels;
|
||||
|
||||
public partial class EditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ICompositor _compositor = new Compositor();
|
||||
private readonly IPaletteService _paletteService = new PaletteService();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private MinintContainer? _container;
|
||||
|
||||
[ObservableProperty]
|
||||
private MinintDocument? _activeDocument;
|
||||
|
||||
[ObservableProperty]
|
||||
private MinintLayer? _activeLayer;
|
||||
|
||||
private bool _suppressDocumentSync;
|
||||
|
||||
[ObservableProperty]
|
||||
private WriteableBitmap? _canvasBitmap;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showGrid;
|
||||
|
||||
/// <summary>
|
||||
/// Path of the currently open file, or null for unsaved new containers.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private string? _filePath;
|
||||
|
||||
public bool HasContainer => Container is not null;
|
||||
|
||||
public string Title => FilePath is not null
|
||||
? $"Minint — {System.IO.Path.GetFileName(FilePath)}"
|
||||
: Container is not null
|
||||
? "Minint — Untitled"
|
||||
: "Minint";
|
||||
|
||||
public ObservableCollection<MinintDocument> Documents { get; } = [];
|
||||
public ObservableCollection<MinintLayer> Layers { get; } = [];
|
||||
|
||||
public void NewContainer(int width, int height)
|
||||
{
|
||||
var c = new MinintContainer(width, height);
|
||||
c.AddNewDocument("Document 1");
|
||||
LoadContainer(c, null);
|
||||
}
|
||||
|
||||
public void LoadContainer(MinintContainer container, string? path)
|
||||
{
|
||||
Container = container;
|
||||
FilePath = path;
|
||||
|
||||
Documents.Clear();
|
||||
foreach (var doc in container.Documents)
|
||||
Documents.Add(doc);
|
||||
|
||||
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by CommunityToolkit when ActiveDocument property changes (e.g. from ListBox binding).
|
||||
/// </summary>
|
||||
partial void OnActiveDocumentChanged(MinintDocument? value)
|
||||
{
|
||||
if (_suppressDocumentSync) return;
|
||||
SyncLayersAndCanvas(value);
|
||||
}
|
||||
|
||||
public void SelectDocument(MinintDocument? doc)
|
||||
{
|
||||
_suppressDocumentSync = true;
|
||||
ActiveDocument = doc;
|
||||
_suppressDocumentSync = false;
|
||||
|
||||
SyncLayersAndCanvas(doc);
|
||||
}
|
||||
|
||||
private void SyncLayersAndCanvas(MinintDocument? doc)
|
||||
{
|
||||
Layers.Clear();
|
||||
if (doc is not null)
|
||||
{
|
||||
foreach (var layer in doc.Layers)
|
||||
Layers.Add(layer);
|
||||
ActiveLayer = doc.Layers.Count > 0 ? doc.Layers[0] : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveLayer = null;
|
||||
}
|
||||
|
||||
RefreshCanvas();
|
||||
}
|
||||
|
||||
public void RefreshCanvas()
|
||||
{
|
||||
if (Container is null || ActiveDocument is null)
|
||||
{
|
||||
CanvasBitmap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
int w = Container.Width;
|
||||
int h = Container.Height;
|
||||
uint[] argb = _compositor.Composite(ActiveDocument, w, h);
|
||||
|
||||
var bmp = new WriteableBitmap(
|
||||
new PixelSize(w, h),
|
||||
new Vector(96, 96),
|
||||
Avalonia.Platform.PixelFormat.Bgra8888);
|
||||
|
||||
using (var fb = bmp.Lock())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var dst = new Span<uint>((void*)fb.Address, w * h);
|
||||
for (int i = 0; i < argb.Length; i++)
|
||||
{
|
||||
// argb[i] is 0xAARRGGBB, need premultiplied BGRA for the bitmap
|
||||
uint px = argb[i];
|
||||
byte a = (byte)(px >> 24);
|
||||
byte r = (byte)((px >> 16) & 0xFF);
|
||||
byte g = (byte)((px >> 8) & 0xFF);
|
||||
byte b = (byte)(px & 0xFF);
|
||||
|
||||
if (a == 255)
|
||||
{
|
||||
dst[i] = px; // ARGB layout == BGRA in LE memory, alpha=255 → no premul needed
|
||||
}
|
||||
else if (a == 0)
|
||||
{
|
||||
dst[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = (byte)(r * a / 255);
|
||||
g = (byte)(g * a / 255);
|
||||
b = (byte)(b * a / 255);
|
||||
dst[i] = (uint)(b | (g << 8) | (r << 16) | (a << 24));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanvasBitmap = bmp;
|
||||
}
|
||||
|
||||
public ICompositor Compositor => _compositor;
|
||||
public IPaletteService PaletteService => _paletteService;
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Minint.Core.Models;
|
||||
using Minint.Core.Services;
|
||||
using Minint.Core.Services.Impl;
|
||||
|
||||
namespace Minint.ViewModels;
|
||||
|
||||
public partial class EditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ICompositor _compositor = new Compositor();
|
||||
private readonly IPaletteService _paletteService = new PaletteService();
|
||||
private readonly IDrawingService _drawingService = new DrawingService();
|
||||
private readonly IFloodFillService _floodFillService = new FloodFillService();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private MinintContainer? _container;
|
||||
|
||||
[ObservableProperty]
|
||||
private MinintDocument? _activeDocument;
|
||||
|
||||
[ObservableProperty]
|
||||
private MinintLayer? _activeLayer;
|
||||
|
||||
private bool _suppressDocumentSync;
|
||||
|
||||
[ObservableProperty]
|
||||
private WriteableBitmap? _canvasBitmap;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showGrid;
|
||||
|
||||
// Tool state
|
||||
[ObservableProperty]
|
||||
private ToolType _activeTool = ToolType.Brush;
|
||||
|
||||
[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;
|
||||
set
|
||||
{
|
||||
if (_previewColor == value) return;
|
||||
_previewColor = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(SelectedColor));
|
||||
}
|
||||
}
|
||||
|
||||
public RgbaColor SelectedColor => new(_previewColor.R, _previewColor.G, _previewColor.B, _previewColor.A);
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private string? _filePath;
|
||||
|
||||
public bool HasContainer => Container is not null;
|
||||
|
||||
public string Title => FilePath is not null
|
||||
? $"Minint — {System.IO.Path.GetFileName(FilePath)}"
|
||||
: Container is not null
|
||||
? "Minint — Untitled"
|
||||
: "Minint";
|
||||
|
||||
public ObservableCollection<MinintDocument> Documents { get; } = [];
|
||||
public ObservableCollection<MinintLayer> Layers { get; } = [];
|
||||
|
||||
#region Container / Document management
|
||||
|
||||
public void NewContainer(int width, int height)
|
||||
{
|
||||
var c = new MinintContainer(width, height);
|
||||
c.AddNewDocument("Document 1");
|
||||
LoadContainer(c, null);
|
||||
}
|
||||
|
||||
public void LoadContainer(MinintContainer container, string? path)
|
||||
{
|
||||
Container = container;
|
||||
FilePath = path;
|
||||
|
||||
Documents.Clear();
|
||||
foreach (var doc in container.Documents)
|
||||
Documents.Add(doc);
|
||||
|
||||
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
|
||||
}
|
||||
|
||||
partial void OnActiveDocumentChanged(MinintDocument? value)
|
||||
{
|
||||
if (_suppressDocumentSync) return;
|
||||
SyncLayersAndCanvas(value);
|
||||
}
|
||||
|
||||
public void SelectDocument(MinintDocument? doc)
|
||||
{
|
||||
_suppressDocumentSync = true;
|
||||
ActiveDocument = doc;
|
||||
_suppressDocumentSync = false;
|
||||
SyncLayersAndCanvas(doc);
|
||||
}
|
||||
|
||||
private void SyncLayersAndCanvas(MinintDocument? doc)
|
||||
{
|
||||
Layers.Clear();
|
||||
if (doc is not null)
|
||||
{
|
||||
foreach (var layer in doc.Layers)
|
||||
Layers.Add(layer);
|
||||
ActiveLayer = doc.Layers.Count > 0 ? doc.Layers[0] : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveLayer = null;
|
||||
}
|
||||
RefreshCanvas();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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)
|
||||
return;
|
||||
|
||||
int w = Container.Width, h = Container.Height;
|
||||
if (px < 0 || px >= w || py < 0 || py >= h)
|
||||
return;
|
||||
|
||||
switch (ActiveTool)
|
||||
{
|
||||
case ToolType.Brush:
|
||||
{
|
||||
int colorIdx = ActiveDocument.EnsureColorCached(SelectedColor);
|
||||
_drawingService.ApplyBrush(ActiveLayer, px, py, BrushRadius, colorIdx, w, h);
|
||||
break;
|
||||
}
|
||||
case ToolType.Eraser:
|
||||
_drawingService.ApplyEraser(ActiveLayer, px, py, BrushRadius, w, h);
|
||||
break;
|
||||
case ToolType.Fill:
|
||||
{
|
||||
int colorIdx = ActiveDocument.EnsureColorCached(SelectedColor);
|
||||
_floodFillService.Fill(ActiveLayer, px, py, colorIdx, w, h);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return null;
|
||||
if (ActiveTool == ToolType.Fill)
|
||||
return null;
|
||||
|
||||
var (cx, cy) = PreviewCenter.Value;
|
||||
return _drawingService.GetBrushMask(cx, cy, BrushRadius, Container.Width, Container.Height);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectBrush() => ActiveTool = ToolType.Brush;
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectEraser() => ActiveTool = ToolType.Eraser;
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectFill() => ActiveTool = ToolType.Fill;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Canvas rendering
|
||||
|
||||
public void RefreshCanvas()
|
||||
{
|
||||
if (Container is null || ActiveDocument is null)
|
||||
{
|
||||
CanvasBitmap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
int w = Container.Width;
|
||||
int h = Container.Height;
|
||||
uint[] argb = _compositor.Composite(ActiveDocument, w, h);
|
||||
|
||||
var bmp = new WriteableBitmap(
|
||||
new PixelSize(w, h),
|
||||
new Vector(96, 96),
|
||||
Avalonia.Platform.PixelFormat.Bgra8888);
|
||||
|
||||
using (var fb = bmp.Lock())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var dst = new Span<uint>((void*)fb.Address, w * h);
|
||||
for (int i = 0; i < argb.Length; i++)
|
||||
{
|
||||
uint px = argb[i];
|
||||
byte a = (byte)(px >> 24);
|
||||
byte r = (byte)((px >> 16) & 0xFF);
|
||||
byte g = (byte)((px >> 8) & 0xFF);
|
||||
byte b = (byte)(px & 0xFF);
|
||||
|
||||
if (a == 255)
|
||||
{
|
||||
dst[i] = px;
|
||||
}
|
||||
else if (a == 0)
|
||||
{
|
||||
dst[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = (byte)(r * a / 255);
|
||||
g = (byte)(g * a / 255);
|
||||
b = (byte)(b * a / 255);
|
||||
dst[i] = (uint)(b | (g << 8) | (r << 16) | (a << 24));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanvasBitmap = bmp;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ICompositor Compositor => _compositor;
|
||||
public IPaletteService PaletteService => _paletteService;
|
||||
public IDrawingService DrawingService => _drawingService;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user