Выделение и вставка
This commit is contained in:
@@ -13,6 +13,11 @@ using Minint.Core.Services.Impl;
|
||||
|
||||
namespace Minint.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Palette-independent clipboard fragment: stores resolved RGBA pixels.
|
||||
/// </summary>
|
||||
public sealed record ClipboardFragment(int Width, int Height, RgbaColor[] Pixels);
|
||||
|
||||
public partial class EditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ICompositor _compositor = new Compositor();
|
||||
@@ -66,6 +71,27 @@ public partial class EditorViewModel : ViewModelBase
|
||||
|
||||
public RgbaColor SelectedColor => new(_previewColor.R, _previewColor.G, _previewColor.B, _previewColor.A);
|
||||
|
||||
// Selection state (Select tool rubber-band)
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasSelection))]
|
||||
private (int X, int Y, int W, int H)? _selectionRect;
|
||||
|
||||
public bool HasSelection => SelectionRect is not null;
|
||||
|
||||
// Clipboard
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasClipboard))]
|
||||
private ClipboardFragment? _clipboard;
|
||||
|
||||
public bool HasClipboard => Clipboard is not null;
|
||||
|
||||
// Paste mode
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsPasting))]
|
||||
private (int X, int Y)? _pastePosition;
|
||||
|
||||
public bool IsPasting => PastePosition is not null;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private string? _filePath;
|
||||
@@ -113,10 +139,6 @@ 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()
|
||||
@@ -191,7 +213,7 @@ public partial class EditorViewModel : ViewModelBase
|
||||
private void RemoveDocument()
|
||||
{
|
||||
if (Container is null || ActiveDocument is null) return;
|
||||
if (Container.Documents.Count <= 1) return; // keep at least one
|
||||
if (Container.Documents.Count <= 1) return;
|
||||
|
||||
var doc = ActiveDocument;
|
||||
int idx = Container.Documents.IndexOf(doc);
|
||||
@@ -203,10 +225,7 @@ public partial class EditorViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RenameDocument()
|
||||
{
|
||||
// Triggered via UI text edit — the Name property is directly editable via TextBox
|
||||
}
|
||||
private void RenameDocument() { }
|
||||
|
||||
[RelayCommand]
|
||||
private void MoveDocumentUp()
|
||||
@@ -254,7 +273,7 @@ public partial class EditorViewModel : ViewModelBase
|
||||
private void RemoveLayer()
|
||||
{
|
||||
if (ActiveDocument is null || ActiveLayer is null) return;
|
||||
if (ActiveDocument.Layers.Count <= 1) return; // keep at least one
|
||||
if (ActiveDocument.Layers.Count <= 1) return;
|
||||
|
||||
UnsubscribeLayerVisibility();
|
||||
var layer = ActiveLayer;
|
||||
@@ -329,6 +348,8 @@ public partial class EditorViewModel : ViewModelBase
|
||||
if (px < 0 || px >= w || py < 0 || py >= h)
|
||||
return;
|
||||
|
||||
if (ActiveTool == ToolType.Select) return; // handled separately
|
||||
|
||||
switch (ActiveTool)
|
||||
{
|
||||
case ToolType.Brush:
|
||||
@@ -353,7 +374,7 @@ public partial class EditorViewModel : ViewModelBase
|
||||
|
||||
public void OnToolDrag(int px, int py)
|
||||
{
|
||||
if (ActiveTool == ToolType.Fill) return;
|
||||
if (ActiveTool is ToolType.Fill or ToolType.Select) return;
|
||||
OnToolDown(px, py);
|
||||
}
|
||||
|
||||
@@ -361,7 +382,7 @@ public partial class EditorViewModel : ViewModelBase
|
||||
{
|
||||
if (PreviewCenter is null || Container is null)
|
||||
return null;
|
||||
if (ActiveTool == ToolType.Fill)
|
||||
if (ActiveTool is ToolType.Fill or ToolType.Select)
|
||||
return null;
|
||||
|
||||
var (cx, cy) = PreviewCenter.Value;
|
||||
@@ -369,36 +390,161 @@ public partial class EditorViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectBrush() => ActiveTool = ToolType.Brush;
|
||||
private void SelectBrush() { CancelPasteMode(); ActiveTool = ToolType.Brush; }
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectEraser() => ActiveTool = ToolType.Eraser;
|
||||
private void SelectEraser() { CancelPasteMode(); ActiveTool = ToolType.Eraser; }
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectFill() => ActiveTool = ToolType.Fill;
|
||||
private void SelectFill() { CancelPasteMode(); ActiveTool = ToolType.Fill; }
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectSelectTool() { CancelPasteMode(); ActiveTool = ToolType.Select; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fragment copy (A4)
|
||||
#region Selection + Copy/Paste (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)
|
||||
/// <summary>Called by PixelCanvas when selection drag starts.</summary>
|
||||
public void BeginSelection(int px, int py)
|
||||
{
|
||||
if (Container is null) return;
|
||||
_fragmentService.CopyFragment(
|
||||
srcDoc, srcLayerIndex, srcX, srcY, regionW, regionH,
|
||||
dstDoc, dstLayerIndex, dstX, dstY,
|
||||
Container.Width, Container.Height);
|
||||
SelectionRect = (px, py, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>Called by PixelCanvas as the user drags.</summary>
|
||||
public void UpdateSelection(int px, int py)
|
||||
{
|
||||
if (SelectionRect is null) return;
|
||||
var s = SelectionRect.Value;
|
||||
int x = Math.Min(s.X, px);
|
||||
int y = Math.Min(s.Y, py);
|
||||
int w = Math.Abs(px - s.X) + 1;
|
||||
int h = Math.Abs(py - s.Y) + 1;
|
||||
// Store normalized rect but keep original anchor in _selAnchor
|
||||
_selectionRectNormalized = (x, y, w, h);
|
||||
}
|
||||
|
||||
/// <summary>Called by PixelCanvas when mouse is released.</summary>
|
||||
public void FinishSelection(int px, int py)
|
||||
{
|
||||
if (SelectionRect is null) return;
|
||||
var s = SelectionRect.Value;
|
||||
int x0 = Math.Min(s.X, px);
|
||||
int y0 = Math.Min(s.Y, py);
|
||||
int rw = Math.Abs(px - s.X) + 1;
|
||||
int rh = Math.Abs(py - s.Y) + 1;
|
||||
if (Container is not null)
|
||||
{
|
||||
x0 = Math.Max(0, x0);
|
||||
y0 = Math.Max(0, y0);
|
||||
rw = Math.Min(rw, Container.Width - x0);
|
||||
rh = Math.Min(rh, Container.Height - y0);
|
||||
}
|
||||
if (rw <= 0 || rh <= 0)
|
||||
{
|
||||
SelectionRect = null;
|
||||
_selectionRectNormalized = null;
|
||||
return;
|
||||
}
|
||||
SelectionRect = (x0, y0, rw, rh);
|
||||
_selectionRectNormalized = SelectionRect;
|
||||
}
|
||||
|
||||
private (int X, int Y, int W, int H)? _selectionRectNormalized;
|
||||
|
||||
/// <summary>The normalized (positive W/H, clamped) selection rectangle for rendering.</summary>
|
||||
public (int X, int Y, int W, int H)? SelectionRectNormalized => _selectionRectNormalized;
|
||||
|
||||
[RelayCommand]
|
||||
private void CopySelection()
|
||||
{
|
||||
if (SelectionRect is null || ActiveDocument is null || ActiveLayer is null || Container is null)
|
||||
return;
|
||||
|
||||
var (sx, sy, sw, sh) = SelectionRect.Value;
|
||||
int cw = Container.Width;
|
||||
var palette = ActiveDocument.Palette;
|
||||
var srcPixels = ActiveLayer.Pixels;
|
||||
var buf = new RgbaColor[sw * sh];
|
||||
|
||||
for (int dy = 0; dy < sh; dy++)
|
||||
{
|
||||
int srcRow = sy + dy;
|
||||
for (int dx = 0; dx < sw; dx++)
|
||||
{
|
||||
int srcCol = sx + dx;
|
||||
int idx = srcPixels[srcRow * cw + srcCol];
|
||||
buf[dy * sw + dx] = idx < palette.Count ? palette[idx] : RgbaColor.Transparent;
|
||||
}
|
||||
}
|
||||
|
||||
Clipboard = new ClipboardFragment(sw, sh, buf);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void PasteClipboard()
|
||||
{
|
||||
if (Clipboard is null) return;
|
||||
PastePosition = (0, 0);
|
||||
}
|
||||
|
||||
public void MovePaste(int px, int py)
|
||||
{
|
||||
if (!IsPasting) return;
|
||||
PastePosition = (px, py);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void CommitPaste()
|
||||
{
|
||||
if (!IsPasting || Clipboard is null || ActiveDocument is null || ActiveLayer is null || Container is null)
|
||||
return;
|
||||
|
||||
var (px, py) = PastePosition!.Value;
|
||||
int cw = Container.Width, ch = Container.Height;
|
||||
var frag = Clipboard;
|
||||
var dstPixels = ActiveLayer.Pixels;
|
||||
|
||||
for (int fy = 0; fy < frag.Height; fy++)
|
||||
{
|
||||
int dy = py + fy;
|
||||
if (dy < 0 || dy >= ch) continue;
|
||||
for (int fx = 0; fx < frag.Width; fx++)
|
||||
{
|
||||
int dx = px + fx;
|
||||
if (dx < 0 || dx >= cw) continue;
|
||||
var color = frag.Pixels[fy * frag.Width + fx];
|
||||
if (color.A == 0) continue; // skip transparent
|
||||
int colorIdx = ActiveDocument.EnsureColorCached(color);
|
||||
dstPixels[dy * cw + dx] = colorIdx;
|
||||
}
|
||||
}
|
||||
|
||||
PastePosition = null;
|
||||
SelectionRect = null;
|
||||
_selectionRectNormalized = null;
|
||||
RefreshCanvas();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void CancelPaste()
|
||||
{
|
||||
PastePosition = null;
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
SelectionRect = null;
|
||||
_selectionRectNormalized = null;
|
||||
}
|
||||
|
||||
private void CancelPasteMode()
|
||||
{
|
||||
PastePosition = null;
|
||||
SelectionRect = null;
|
||||
_selectionRectNormalized = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Canvas rendering
|
||||
|
||||
Reference in New Issue
Block a user