Этап 5

This commit is contained in:
2026-03-29 16:05:45 +03:00
parent 5fdaaaa2bf
commit 400af7982e
8 changed files with 474 additions and 34 deletions

View File

@@ -0,0 +1,160 @@
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;
/// <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;
}