浏览代码

Canvas preview rendering

Equbuxu 3 年之前
父节点
当前提交
d17ed83735

+ 25 - 19
src/PixiEditorPrototype/Models/ActionAccumulator.cs

@@ -120,6 +120,7 @@ internal class ActionAccumulator
             if (child is FolderViewModel innerFolder)
                 LockPreviewBitmaps(innerFolder);
         }
+        document.PreviewBitmap.Lock();
     }
 
     private void UnlockPreviewBitmaps(FolderViewModel root)
@@ -132,6 +133,7 @@ internal class ActionAccumulator
             if (child is FolderViewModel innerFolder)
                 UnlockPreviewBitmaps(innerFolder);
         }
+        document.PreviewBitmap.Unlock();
     }
 
     private void AddDirtyRects(List<IRenderInfo> changes)
@@ -141,32 +143,36 @@ internal class ActionAccumulator
             switch (renderInfo)
             {
                 case DirtyRect_RenderInfo info:
-                    {
-                        var bitmap = document.Bitmaps[info.Resolution];
-                        RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelWidth, bitmap.PixelHeight));
+                {
+                    var bitmap = document.Bitmaps[info.Resolution];
+                    RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelWidth, bitmap.PixelHeight));
 
-                        RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
-                        bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
-                    }
+                    RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
+                    bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
+                }
                     break;
                 case PreviewDirty_RenderInfo info:
-                    {
-                        var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.PreviewBitmap;
-                        if (bitmap is null)
-                            continue;
-                        bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
-                    }
+                {
+                    var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.PreviewBitmap;
+                    if (bitmap is null)
+                        continue;
+                    bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
+                }
                     break;
                 case MaskPreviewDirty_RenderInfo info:
-                    {
-                        var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.MaskPreviewBitmap;
-                        if (bitmap is null)
-                            continue;
-                        bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
-                    }
+                {
+                    var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.MaskPreviewBitmap;
+                    if (bitmap is null)
+                        continue;
+                    bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
+                }
+                    break;
+                case CanvasPreviewDirty_RenderInfo:
+                {
+                    document.PreviewBitmap.AddDirtyRect(new Int32Rect(0, 0, document.PreviewBitmap.PixelWidth, document.PreviewBitmap.PixelHeight));
+                }
                     break;
             }
         }
-
     }
 }

+ 7 - 1
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -18,6 +18,7 @@ internal class DocumentUpdater
 {
     private DocumentViewModel doc;
     private DocumentHelpers helper;
+
     public DocumentUpdater(DocumentViewModel doc, DocumentHelpers helper)
     {
         this.doc = doc;
@@ -202,9 +203,14 @@ internal class DocumentUpdater
         doc.SetVerticalSymmetryAxisX(info.VerticalSymmetryAxisX);
         doc.SetHorizontalSymmetryAxisY(info.HorizontalSymmetryAxisY);
 
+        var previewSize = StructureMemberViewModel.CalculatePreviewSize(info.Size);
+        doc.PreviewSurface.Dispose();
+        doc.PreviewBitmap = CreateBitmap(previewSize);
+        doc.PreviewSurface = CreateSKSurface(doc.PreviewBitmap);
+
         doc.RaisePropertyChanged(nameof(doc.Bitmaps));
+        doc.RaisePropertyChanged(nameof(doc.PreviewBitmap));
 
-        var previewSize = StructureMemberViewModel.CalculatePreviewSize(info.Size);
         UpdateMemberBitmapsRecursively(doc.StructureRoot, previewSize);
     }
 

+ 3 - 0
src/PixiEditorPrototype/Models/Rendering/RenderInfos/CanvasPreviewDirty_RenderInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditorPrototype.Models.Rendering.RenderInfos;
+
+internal record CanvasPreviewDirty_RenderInfo : IRenderInfo;

+ 63 - 42
src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading.Tasks;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
@@ -22,30 +23,26 @@ internal class WriteableBitmapUpdater
     private static readonly SKPaint SmoothReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.Medium, IsAntialias = true };
     private static readonly SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
 
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedChunks = new()
-    {
-        [ChunkResolution.Full] = new(),
-        [ChunkResolution.Half] = new(),
-        [ChunkResolution.Quarter] = new(),
-        [ChunkResolution.Eighth] = new()
-    };
+    /// <summary>
+    /// Chunks that have been updated but don't need to be re-rendered because they are out of view
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+    /// <summary>
+    /// The state of globalPostponedChunks during the last update of global delayed chunks (when you finish using a tool)
+    /// It is required in case the viewport is moved while you are using a tool. In this case the newly visible chunks on delayed viewports
+    /// need to be re-rendered, even though normally re-render only happens after you're done with some tool.
+    /// Because the viewport still has the old version of the image there is no point in re-rendering everything from globalPostponedChunks.
+    /// It's enough to re-render the chunks that were postponed back when the delayed viewports were last updated fully.
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedForDelayed = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+    /// <summary>
+    /// Chunks that have been updated but don't need to be re-rendered because all viewports that see them have Delayed == true
+    /// These chunks are updated after you finish using a tool
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalDelayedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
 
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedForDelayed = new()
-    {
-        [ChunkResolution.Full] = new(),
-        [ChunkResolution.Half] = new(),
-        [ChunkResolution.Quarter] = new(),
-        [ChunkResolution.Eighth] = new()
-    };
-    
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalDelayedChunks = new()
-    {
-        [ChunkResolution.Full] = new(),
-        [ChunkResolution.Half] = new(),
-        [ChunkResolution.Quarter] = new(),
-        [ChunkResolution.Eighth] = new()
-    };
-    
     private Dictionary<Guid, HashSet<VecI>> previewDelayedChunks = new();
     private Dictionary<Guid, HashSet<VecI>> maskPreviewDelayedChunks = new();
 
@@ -73,21 +70,9 @@ internal class WriteableBitmapUpdater
         }
 
         // find all chunks that are on viewports and on delayed viewports
-        var chunksToUpdate = new Dictionary<ChunkResolution, HashSet<VecI>>()
-        {
-            [ChunkResolution.Full] = new(),
-            [ChunkResolution.Half] = new(),
-            [ChunkResolution.Quarter] = new(),
-            [ChunkResolution.Eighth] = new()
-        };
-        
-        var chunksOnDelayedViewports = new Dictionary<ChunkResolution, HashSet<VecI>>()
-        {
-            [ChunkResolution.Full] = new(),
-            [ChunkResolution.Half] = new(),
-            [ChunkResolution.Quarter] = new(),
-            [ChunkResolution.Eighth] = new()
-        };
+        var chunksToUpdate = new Dictionary<ChunkResolution, HashSet<VecI>>() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+        var chunksOnDelayedViewports = new Dictionary<ChunkResolution, HashSet<VecI>>() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
 
         foreach (var (_, viewport) in helpers.State.Viewports)
         {
@@ -109,7 +94,7 @@ internal class WriteableBitmapUpdater
             chunksOnDelayedViewports[res].IntersectWith(postponed);
             postponed.ExceptWith(chunksToUpdate[res]);
         }
-        
+
         // decide what to do about the delayed chunks
         if (renderDelayed)
         {
@@ -126,7 +111,7 @@ internal class WriteableBitmapUpdater
             {
                 chunksOnDelayedViewports[res].IntersectWith(globalPostponedForDelayed[res]);
                 globalPostponedForDelayed[res].ExceptWith(chunksOnDelayedViewports[res]);
-                
+
                 chunksToUpdate[res].UnionWith(chunksOnDelayedViewports[res]);
                 postponed.ExceptWith(chunksOnDelayedViewports[res]);
             }
@@ -145,6 +130,7 @@ internal class WriteableBitmapUpdater
             to[guid].UnionWith(chunks);
         }
     }
+
     private (Dictionary<Guid, HashSet<VecI>> image, Dictionary<Guid, HashSet<VecI>> mask) FindPreviewChunksToRerender
         (AffectedChunkGatherer chunkGatherer, bool postpone)
     {
@@ -176,6 +162,41 @@ internal class WriteableBitmapUpdater
 
     private void UpdateImagePreviews(Dictionary<Guid, HashSet<VecI>> imagePreviewChunks, float scaling, List<IRenderInfo> infos)
     {
+        // update preview of the whole canvas
+        var cumulative = imagePreviewChunks.Aggregate(new HashSet<VecI>(), (set, pair) =>
+        {
+            set.UnionWith(pair.Value);
+            return set;
+        });
+        bool somethingChanged = false;
+        foreach (var chunkPos in cumulative)
+        {
+            somethingChanged = true;
+            ChunkResolution resolution = scaling switch
+            {
+                > 1 / 2f => ChunkResolution.Full,
+                > 1 / 4f => ChunkResolution.Half,
+                > 1 / 8f => ChunkResolution.Quarter,
+                _ => ChunkResolution.Eighth,
+            };
+            var pos = chunkPos * resolution.PixelSize();
+            var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution, helpers.Tracker.Document.StructureRoot);
+            doc.PreviewSurface.Canvas.Save();
+            doc.PreviewSurface.Canvas.Scale(scaling);
+            doc.PreviewSurface.Canvas.Scale(1 / (float)resolution.Multiplier());
+            if (rendered.IsT1)
+            {
+                doc.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
+                return;
+            }
+            using var renderedChunk = rendered.AsT0;
+            renderedChunk.DrawOnSurface(doc.PreviewSurface, pos, SmoothReplacingPaint);
+            doc.PreviewSurface.Canvas.Restore();
+        }
+        if (somethingChanged)
+            infos.Add(new CanvasPreviewDirty_RenderInfo());
+
+        // update previews of individual members
         foreach (var (guid, chunks) in imagePreviewChunks)
         {
             var memberVM = helpers.StructureHelper.Find(guid);
@@ -193,7 +214,7 @@ internal class WriteableBitmapUpdater
                     var pos = chunk * ChunkResolution.Full.PixelSize();
                     // the full res chunks are already rendered so drawing them again should be fast
                     if (!layer.LayerImage.DrawMostUpToDateChunkOn
-                        (chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, SmoothReplacingPaint))
+                            (chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, SmoothReplacingPaint))
                         memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
                 }
                 infos.Add(new PreviewDirty_RenderInfo(guid));
@@ -260,7 +281,7 @@ internal class WriteableBitmapUpdater
                     chunkPos * chunkSize,
                     new(chunkSize, chunkSize),
                     resolution
-                    ));
+                ));
             }
         }
     }

+ 22 - 16
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -8,7 +8,6 @@ using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using Microsoft.Win32;
-using OneOf;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Enums;
@@ -139,6 +138,9 @@ internal class DocumentViewModel : INotifyPropertyChanged
         [ChunkResolution.Full] = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null), [ChunkResolution.Half] = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Pbgra32, null), [ChunkResolution.Quarter] = new WriteableBitmap(16, 16, 96, 96, PixelFormats.Pbgra32, null), [ChunkResolution.Eighth] = new WriteableBitmap(8, 8, 96, 96, PixelFormats.Pbgra32, null),
     };
 
+    public WriteableBitmap PreviewBitmap { get; set; }
+    public SKSurface PreviewSurface { get; set; }
+
     public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
     public FolderViewModel StructureRoot { get; }
     public DocumentTransformViewModel TransformViewModel { get; }
@@ -218,6 +220,10 @@ internal class DocumentViewModel : INotifyPropertyChanged
                 bitmap.Value.BackBuffer, bitmap.Value.BackBufferStride);
             Surfaces[bitmap.Key] = surface;
         }
+
+        var previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
+        PreviewBitmap = new WriteableBitmap(previewSize.X, previewSize.Y, 96, 96, PixelFormats.Pbgra32, null);
+        PreviewSurface = SKSurface.Create(new SKImageInfo(previewSize.X, previewSize.Y, SKColorType.Bgra8888), PreviewBitmap.BackBuffer, PreviewBitmap.BackBufferStride);
     }
 
     public static DocumentViewModel FromSerializableDocument(ViewModelMain owner, SerializableDocument serDocument, string name)
@@ -414,7 +420,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
         updateableChangeActive = true;
         var member = FindFirstSelectedMember();
         Helpers.ActionAccumulator.AddActions(
-            new DrawLine_Action(member!.GuidValue, from, to ,strokeWidth, color, cap, member.ShouldDrawOnMask));
+            new DrawLine_Action(member!.GuidValue, from, to, strokeWidth, color, cap, member.ShouldDrawOnMask));
     }
 
     public void EndLine()
@@ -425,7 +431,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
         updateableChangeActive = false;
         Helpers.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
     }
-    
+
     public void StartUpdateEllipse(RectI location, SKColor strokeColor, SKColor fillColor, int strokeWidth)
     {
         if (!CanStartUpdate())
@@ -525,7 +531,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
     }
 
     public SKColor PickColor(VecI pos, bool fromAllLayers)
-    { 
+    {
         // there is a tiny chance that the image might get disposed by another thread
         try
         {
@@ -534,19 +540,19 @@ internal class DocumentViewModel : INotifyPropertyChanged
             if (fromAllLayers)
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                    return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Helpers.Tracker.Document.StructureRoot)
-                        .Match<SKColor>(
-                            (Chunk chunk) =>
-                            {
-                                VecI posOnChunk = pos - chunkPos * ChunkyImage.FullChunkSize;
-                                var color = chunk.Surface.GetSRGBPixel(posOnChunk);
-                                chunk.Dispose();
-                                return color;
-                            },
-                            _ => SKColors.Transparent
-                        );
+                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Helpers.Tracker.Document.StructureRoot)
+                    .Match<SKColor>(
+                        (Chunk chunk) =>
+                        {
+                            VecI posOnChunk = pos - chunkPos * ChunkyImage.FullChunkSize;
+                            var color = chunk.Surface.GetSRGBPixel(posOnChunk);
+                            chunk.Dispose();
+                            return color;
+                        },
+                        _ => SKColors.Transparent
+                    );
             }
-            
+
             if (SelectedStructureMember is not LayerViewModel layerVm)
                 return SKColors.Transparent;
             var maybeMember = Helpers.Tracker.Document.FindMember(layerVm.GuidValue);

+ 195 - 188
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -400,192 +400,192 @@
             BorderBrush="Black"
             DockPanel.Dock="Top"
             Margin="5">
-                <WrapPanel
-                    Orientation="Horizontal"
-                    Background="White">
-                    <Button
-                        Width="50"
-                        Margin="5"
-                        Command="{Binding LoadDocumentCommand}">
-                        Open
-                    </Button>
-                    <Button
-                        Width="50"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.UndoCommand}">
-                        Undo
-                    </Button>
-                    <Button
-                        Width="50"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.RedoCommand}">
-                        Redo
-                    </Button>
-                    <Button
-                        Width="100"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.ClearSelectionCommand}">
-                        Clear selection
-                    </Button>
-                    <Button
-                        Width="110"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.TransformSelectionPathCommand}">
-                        Transform sel. path
-                    </Button>
-                    <Button
-                        Width="110"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.TransformSelectedAreaCommand}">
-                        Transform sel. area
-                    </Button>
-                    <ComboBox
-                        Width="70"
-                        Height="20"
-                        Margin="5"
-                        SelectedIndex="0"
-                        x:Name="selectionModeComboBox">
-                        <ComboBoxItem
-                            Tag="{x:Static chen:SelectionMode.New}">
-                            New
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:SelectionMode.Add}">
-                            Add
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:SelectionMode.Subtract}">
-                            Subtract
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:SelectionMode.Intersect}">
-                            Intersect
-                        </ComboBoxItem>
-                        <i:Interaction.Triggers>
-                            <i:EventTrigger
-                                EventName="SelectionChanged">
-                                <i:InvokeCommandAction
-                                    Command="{Binding SetSelectionModeCommand}"
-                                    CommandParameter="{Binding SelectedItem.Tag, ElementName=selectionModeComboBox}" />
-                            </i:EventTrigger>
-                        </i:Interaction.Triggers>
-                    </ComboBox>
-                    <ComboBox
-                        Width="70"
-                        Height="20"
-                        Margin="5"
-                        SelectedIndex="0"
-                        x:Name="lineCapComboBox">
-                        <ComboBoxItem
-                            Tag="{x:Static sk:SKStrokeCap.Butt}">
-                            Butt
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static sk:SKStrokeCap.Round}">
-                            Round
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static sk:SKStrokeCap.Square}">
-                            Square
-                        </ComboBoxItem>
-                        <i:Interaction.Triggers>
-                            <i:EventTrigger
-                                EventName="SelectionChanged">
-                                <i:InvokeCommandAction
-                                    Command="{Binding SetLineCapCommand}"
-                                    CommandParameter="{Binding SelectedItem.Tag, ElementName=lineCapComboBox}" />
-                            </i:EventTrigger>
-                        </i:Interaction.Triggers>
-                    </ComboBox>
-                    <ComboBox
-                        Width="70"
-                        Height="20"
-                        Margin="5"
-                        SelectedIndex="0"
-                        x:Name="resizeAnchorComboBox">
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.TopLeft}">
-                            Top Left
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.Top}">
-                            Top
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.TopRight}">
-                            Top Right
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.Left}">
-                            Left
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.Center}">
-                            Center
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.Right}">
-                            Right
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.BottomLeft}">
-                            Bottom Left
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.Bottom}">
-                            Bottom
-                        </ComboBoxItem>
-                        <ComboBoxItem
-                            Tag="{x:Static chen:ResizeAnchor.BottomRight}">
-                            Bottom Right
-                        </ComboBoxItem>
-                        <i:Interaction.Triggers>
-                            <i:EventTrigger
-                                EventName="SelectionChanged">
-                                <i:InvokeCommandAction
-                                    Command="{Binding SetResizeAnchorCommand}"
-                                    CommandParameter="{Binding SelectedItem.Tag, ElementName=resizeAnchorComboBox}" />
-                            </i:EventTrigger>
-                        </i:Interaction.Triggers>
-                    </ComboBox>
-                    <Button
-                        Width="120"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.ClearHistoryCommand}">
-                        Clear undo history
-                    </Button>
-                    <Button
-                        Width="100"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.PasteImageCommand}">
-                        Paste Image
-                    </Button>
-                    <Button
-                        Width="100"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.ApplyTransformCommand}">
-                        Apply Transform
-                    </Button>
-                    <Label>Pen size:</Label>
-                    <TextBox
-                        Width="30"
-                        Margin="5"
-                        Text="{Binding StrokeWidth}" />
-                    <TextBox
-                        Width="30"
-                        Margin="5"
-                        Text="{Binding ActiveDocument.ResizeWidth}" />
-                    <TextBox
-                        Width="30"
-                        Margin="5"
-                        Text="{Binding ActiveDocument.ResizeHeight}" />
-                    <Button
-                        Width="50"
-                        Margin="5"
-                        Command="{Binding ActiveDocument.ResizeCanvasCommand}">
-                        Resize
-                    </Button>
-                </WrapPanel>
+            <WrapPanel
+                Orientation="Horizontal"
+                Background="White">
+                <Button
+                    Width="50"
+                    Margin="5"
+                    Command="{Binding LoadDocumentCommand}">
+                    Open
+                </Button>
+                <Button
+                    Width="50"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.UndoCommand}">
+                    Undo
+                </Button>
+                <Button
+                    Width="50"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.RedoCommand}">
+                    Redo
+                </Button>
+                <Button
+                    Width="100"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.ClearSelectionCommand}">
+                    Clear selection
+                </Button>
+                <Button
+                    Width="110"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.TransformSelectionPathCommand}">
+                    Transform sel. path
+                </Button>
+                <Button
+                    Width="110"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.TransformSelectedAreaCommand}">
+                    Transform sel. area
+                </Button>
+                <ComboBox
+                    Width="70"
+                    Height="20"
+                    Margin="5"
+                    SelectedIndex="0"
+                    x:Name="selectionModeComboBox">
+                    <ComboBoxItem
+                        Tag="{x:Static chen:SelectionMode.New}">
+                        New
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:SelectionMode.Add}">
+                        Add
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:SelectionMode.Subtract}">
+                        Subtract
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:SelectionMode.Intersect}">
+                        Intersect
+                    </ComboBoxItem>
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger
+                            EventName="SelectionChanged">
+                            <i:InvokeCommandAction
+                                Command="{Binding SetSelectionModeCommand}"
+                                CommandParameter="{Binding SelectedItem.Tag, ElementName=selectionModeComboBox}" />
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </ComboBox>
+                <ComboBox
+                    Width="70"
+                    Height="20"
+                    Margin="5"
+                    SelectedIndex="0"
+                    x:Name="lineCapComboBox">
+                    <ComboBoxItem
+                        Tag="{x:Static sk:SKStrokeCap.Butt}">
+                        Butt
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static sk:SKStrokeCap.Round}">
+                        Round
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static sk:SKStrokeCap.Square}">
+                        Square
+                    </ComboBoxItem>
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger
+                            EventName="SelectionChanged">
+                            <i:InvokeCommandAction
+                                Command="{Binding SetLineCapCommand}"
+                                CommandParameter="{Binding SelectedItem.Tag, ElementName=lineCapComboBox}" />
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </ComboBox>
+                <ComboBox
+                    Width="70"
+                    Height="20"
+                    Margin="5"
+                    SelectedIndex="0"
+                    x:Name="resizeAnchorComboBox">
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.TopLeft}">
+                        Top Left
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.Top}">
+                        Top
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.TopRight}">
+                        Top Right
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.Left}">
+                        Left
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.Center}">
+                        Center
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.Right}">
+                        Right
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.BottomLeft}">
+                        Bottom Left
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.Bottom}">
+                        Bottom
+                    </ComboBoxItem>
+                    <ComboBoxItem
+                        Tag="{x:Static chen:ResizeAnchor.BottomRight}">
+                        Bottom Right
+                    </ComboBoxItem>
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger
+                            EventName="SelectionChanged">
+                            <i:InvokeCommandAction
+                                Command="{Binding SetResizeAnchorCommand}"
+                                CommandParameter="{Binding SelectedItem.Tag, ElementName=resizeAnchorComboBox}" />
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </ComboBox>
+                <Button
+                    Width="120"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.ClearHistoryCommand}">
+                    Clear undo history
+                </Button>
+                <Button
+                    Width="100"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.PasteImageCommand}">
+                    Paste Image
+                </Button>
+                <Button
+                    Width="100"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.ApplyTransformCommand}">
+                    Apply Transform
+                </Button>
+                <Label>Pen size:</Label>
+                <TextBox
+                    Width="30"
+                    Margin="5"
+                    Text="{Binding StrokeWidth}" />
+                <TextBox
+                    Width="30"
+                    Margin="5"
+                    Text="{Binding ActiveDocument.ResizeWidth}" />
+                <TextBox
+                    Width="30"
+                    Margin="5"
+                    Text="{Binding ActiveDocument.ResizeHeight}" />
+                <Button
+                    Width="50"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.ResizeCanvasCommand}">
+                    Resize
+                </Button>
+            </WrapPanel>
         </Border>
         <Border
             BorderThickness="1"
@@ -750,8 +750,15 @@
             SelectedIndex="{Binding ActiveDocumentIndex}">
             <TabControl.ItemTemplate>
                 <DataTemplate>
-                    <TextBlock
-                        Text="{Binding Name}" />
+                    <StackPanel
+                        Orientation="Horizontal">
+                        <Image
+                            Source="{Binding PreviewBitmap}"
+                            Width="30"
+                            Height="30" />
+                        <TextBlock
+                            Text="{Binding Name}" />
+                    </StackPanel>
                 </DataTemplate>
             </TabControl.ItemTemplate>
             <TabControl.ContentTemplate>

+ 1 - 1
src/README.md

@@ -77,7 +77,7 @@ Decouples the state of a document from the UI.
         - Rendering images for changes (tools requiring final image, merge layers, etc.)
             - [x] ChunkRenderer as a part of Document
         - [x] Rendering of layer previews
-        - [ ] Rendering of canvas previews
+        - [x] Rendering of canvas previews
         - [x] Support for multiple viewports
     - Changes
         - [x] Create/Delete/Move structure members