Browse Source

Basic rendering and drawing

Equbuxu 3 years ago
parent
commit
c99d9f1cfc

+ 2 - 2
src/ChangeableDocument/Actions/Drawing/RectangleActions.cs

@@ -4,7 +4,7 @@ using ChunkyImageLib.DataHolders;
 
 namespace ChangeableDocument.Actions.Drawing
 {
-    public record DrawRectangle_Action : IStartOrUpdateChangeAction
+    public record struct DrawRectangle_Action : IStartOrUpdateChangeAction
     {
         public DrawRectangle_Action(Guid layerGuid, ShapeData rectangle)
         {
@@ -26,7 +26,7 @@ namespace ChangeableDocument.Actions.Drawing
         }
     }
 
-    public record EndDrawRectangle_Action : IEndChangeAction
+    public record struct EndDrawRectangle_Action : IEndChangeAction
     {
         bool IEndChangeAction.IsChangeTypeMatching(IChange change)
         {

+ 2 - 2
src/ChangeableDocument/Actions/MiscActions.cs

@@ -1,9 +1,9 @@
 namespace ChangeableDocument.Actions;
 
-public record RedoAction : IAction
+public record struct RedoAction : IAction
 {
 }
 
-public record UndoAction : IAction
+public record struct UndoAction : IAction
 {
 }

+ 2 - 2
src/ChangeableDocument/Actions/OpacityActions.cs

@@ -2,7 +2,7 @@
 
 namespace ChangeableDocument.Actions
 {
-    public record OpacityChange_Action : IStartOrUpdateChangeAction
+    public record struct OpacityChange_Action : IStartOrUpdateChangeAction
     {
         public OpacityChange_Action(Guid memberGuid, float opacity)
         {
@@ -24,7 +24,7 @@ namespace ChangeableDocument.Actions
         }
     }
 
-    public record EndOpacityChange_Action : IEndChangeAction
+    public record struct EndOpacityChange_Action : IEndChangeAction
     {
         bool IEndChangeAction.IsChangeTypeMatching(IChange change) => change is StructureMemberOpacity_UpdateableChange;
     }

+ 5 - 5
src/ChangeableDocument/Actions/StructureActions.cs

@@ -1,7 +1,7 @@
 using ChangeableDocument.Changes;
 
 namespace ChangeableDocument.Actions;
-public record CreateStructureMember_Action : IMakeChangeAction
+public record struct CreateStructureMember_Action : IMakeChangeAction
 {
     public CreateStructureMember_Action(Guid parentGuid, int index, StructureMemberType type)
     {
@@ -20,7 +20,7 @@ public record CreateStructureMember_Action : IMakeChangeAction
     }
 }
 
-public record DeleteStructureMember_Action : IMakeChangeAction
+public record struct DeleteStructureMember_Action : IMakeChangeAction
 {
     public DeleteStructureMember_Action(Guid guidValue)
     {
@@ -35,7 +35,7 @@ public record DeleteStructureMember_Action : IMakeChangeAction
     }
 }
 
-public record MoveStructureMember_Action : IMakeChangeAction
+public record struct MoveStructureMember_Action : IMakeChangeAction
 {
     public MoveStructureMember_Action(Guid member, Guid targetFolder, int index)
     {
@@ -54,7 +54,7 @@ public record MoveStructureMember_Action : IMakeChangeAction
     }
 }
 
-public record SetStructureMemberName_Action : IMakeChangeAction
+public record struct SetStructureMemberName_Action : IMakeChangeAction
 {
     public SetStructureMemberName_Action(string name, Guid guidValue)
     {
@@ -71,7 +71,7 @@ public record SetStructureMemberName_Action : IMakeChangeAction
     }
 }
 
-public record SetStructureMemberVisibility_Action : IMakeChangeAction
+public record struct SetStructureMemberVisibility_Action : IMakeChangeAction
 {
     public SetStructureMemberVisibility_Action(bool isVisible, Guid guidValue)
     {

+ 1 - 1
src/ChangeableDocument/ChangeInfos/LayerImageChunks_ChangeInfo.cs

@@ -1,6 +1,6 @@
 namespace ChangeableDocument.ChangeInfos
 {
-    public record LayerImageChunks_ChangeInfo : IChangeInfo
+    public record struct LayerImageChunks_ChangeInfo : IChangeInfo
     {
         public Guid LayerGuid { get; init; }
         public HashSet<(int, int)>? Chunks { get; init; }

+ 3 - 3
src/ChangeableDocument/ChangeInfos/StructureChangeInfos.cs

@@ -1,16 +1,16 @@
 namespace ChangeableDocument.ChangeInfos
 {
-    public record CreateStructureMember_ChangeInfo : IChangeInfo
+    public record struct CreateStructureMember_ChangeInfo : IChangeInfo
     {
         public Guid GuidValue { get; init; }
     }
 
-    public record DeleteStructureMember_ChangeInfo : IChangeInfo
+    public record struct DeleteStructureMember_ChangeInfo : IChangeInfo
     {
         public Guid GuidValue { get; init; }
     }
 
-    public record MoveStructureMember_ChangeInfo : IChangeInfo
+    public record struct MoveStructureMember_ChangeInfo : IChangeInfo
     {
         public Guid GuidValue { get; init; }
     }

+ 1 - 1
src/ChangeableDocument/ChangeInfos/StructureMemberOpacity_ChangeInfo.cs

@@ -1,6 +1,6 @@
 namespace ChangeableDocument.ChangeInfos
 {
-    public record StructureMemberOpacity_ChangeInfo : IChangeInfo
+    public record struct StructureMemberOpacity_ChangeInfo : IChangeInfo
     {
         public Guid GuidValue { get; init; }
     }

+ 1 - 1
src/ChangeableDocument/ChangeInfos/StructureMemberProperties_ChangeInfo.cs

@@ -1,6 +1,6 @@
 namespace ChangeableDocument.ChangeInfos
 {
-    public record StructureMemberProperties_ChangeInfo : IChangeInfo
+    public record struct StructureMemberProperties_ChangeInfo : IChangeInfo
     {
         public Guid GuidValue { get; init; }
         public bool IsVisibleChanged { get; init; } = false;

+ 4 - 1
src/ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs

@@ -26,11 +26,14 @@ namespace ChangeableDocument.Changes.Drawing
         public IChangeInfo? ApplyTemporarily(Document target)
         {
             Layer layer = (Layer)target.FindMemberOrThrow(layerGuid);
+            var oldChunks = layer.LayerImage.FindAffectedChunks();
             layer.LayerImage.CancelChanges();
             layer.LayerImage.DrawRectangle(rect);
+            var newChunks = layer.LayerImage.FindAffectedChunks();
+            newChunks.UnionWith(oldChunks);
             return new LayerImageChunks_ChangeInfo()
             {
-                Chunks = layer.LayerImage.FindAffectedChunks(),
+                Chunks = newChunks,
                 LayerGuid = layerGuid
             };
         }

+ 1 - 1
src/ChunkyImageLib/Chunk.cs

@@ -7,7 +7,7 @@ namespace ChunkyImageLib
         internal Surface Surface { get; }
         internal Chunk()
         {
-            Surface = new Surface(ChunkPool.ChunkSize, ChunkPool.ChunkSize, SkiaSharp.SKColorType.RgbaF16);
+            Surface = new Surface(ChunkPool.ChunkSize, ChunkPool.ChunkSize);
         }
 
         public SKImage Snapshot()

+ 1 - 1
src/ChunkyImageLib/ChunkPool.cs

@@ -2,9 +2,9 @@
 {
     internal class ChunkPool
     {
-        // not thread-safe!
         public const int ChunkSize = 32;
 
+        // not thread-safe!
         private static ChunkPool? instance;
         public static ChunkPool Instance => instance ??= new ChunkPool();
 

+ 3 - 3
src/ChunkyImageLib/ChunkStorage.cs

@@ -4,11 +4,11 @@
     {
         private bool disposed = false;
         private List<(int, int, Chunk?)> savedChunks = new();
-        public ChunkStorage(ChunkyImage image, HashSet<(int, int)> chunksToSave)
+        public ChunkStorage(ChunkyImage image, HashSet<(int, int)> commitedChunksToSave)
         {
-            foreach (var (x, y) in chunksToSave)
+            foreach (var (x, y) in commitedChunksToSave)
             {
-                Chunk? chunk = image.GetChunk(x, y);
+                Chunk? chunk = image.GetCommitedChunk(x, y);
                 if (chunk == null)
                 {
                     savedChunks.Add((x, y, null));

+ 19 - 3
src/ChunkyImageLib/ChunkyImage.cs

@@ -14,6 +14,8 @@ namespace ChunkyImageLib
         private Dictionary<(int, int), Chunk> chunks = new();
         private Dictionary<(int, int), Chunk> uncommitedChunks = new();
 
+        public static int ChunkSize => ChunkPool.ChunkSize;
+
         public Chunk? GetChunk(int x, int y)
         {
             if (queuedOperations.Count == 0)
@@ -22,6 +24,11 @@ namespace ChunkyImageLib
             return MaybeGetChunk(x, y, uncommitedChunks) ?? MaybeGetChunk(x, y, chunks);
         }
 
+        public Chunk? GetCommitedChunk(int x, int y)
+        {
+            return MaybeGetChunk(x, y, chunks);
+        }
+
         private Chunk? MaybeGetChunk(int x, int y, Dictionary<(int, int), Chunk> from) => from.ContainsKey((x, y)) ? from[(x, y)] : null;
 
         public void DrawRectangle(ShapeData rect)
@@ -45,6 +52,7 @@ namespace ChunkyImageLib
             {
                 ChunkPool.Instance.ReturnChunk(chunk);
             }
+            uncommitedChunks.Clear();
         }
 
         public void CommitChanges()
@@ -55,7 +63,12 @@ namespace ChunkyImageLib
 
         public HashSet<(int, int)> FindAffectedChunks()
         {
-            return uncommitedChunks.Select(chunk => chunk.Key).ToHashSet();
+            var chunks = uncommitedChunks.Select(chunk => chunk.Key).ToHashSet();
+            foreach (var (operation, opChunks) in queuedOperations)
+            {
+                chunks.UnionWith(opChunks);
+            }
+            return chunks;
         }
 
         private void ProcessQueueFinal()
@@ -83,6 +96,7 @@ namespace ChunkyImageLib
                 }
                 chunks.Add(pos, chunk);
             }
+            uncommitedChunks.Clear();
         }
 
         private void ProcessQueue(int chunkX, int chunkY)
@@ -108,7 +122,7 @@ namespace ChunkyImageLib
                 return targetChunk;
             var newChunk = ChunkPool.Instance.BorrowChunk();
             newChunk.Surface.SkiaSurface.Canvas.Clear();
-            chunks.Add((chunkX, chunkY), newChunk);
+            chunks[(chunkX, chunkY)] = newChunk;
             return newChunk;
         }
 
@@ -120,12 +134,14 @@ namespace ChunkyImageLib
             {
                 targetChunk = ChunkPool.Instance.BorrowChunk();
                 var maybeCommitedChunk = MaybeGetChunk(chunkX, chunkY, chunks);
+
                 if (maybeCommitedChunk != null)
                     maybeCommitedChunk.Surface.CopyTo(targetChunk.Surface);
                 else
                     targetChunk.Surface.SkiaSurface.Canvas.Clear();
+
+                uncommitedChunks[(chunkX, chunkY)] = targetChunk;
             }
-            uncommitedChunks.Add((chunkX, chunkY), targetChunk);
             return targetChunk;
         }
     }

+ 1 - 1
src/ChunkyImageLib/DataHolders/ShapeData.cs

@@ -2,7 +2,7 @@
 
 namespace ChunkyImageLib.DataHolders
 {
-    public record ShapeData
+    public record struct ShapeData
     {
         public ShapeData(int x, int y, int width, int height, int strokeWidth, SKColor strokeColor, SKColor fillColor)
         {

+ 6 - 3
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -1,10 +1,13 @@
-namespace ChunkyImageLib.Operations
+using SkiaSharp;
+
+namespace ChunkyImageLib.Operations
 {
-    internal record ImageOperation : IOperation
+    internal record class ImageOperation : IOperation
     {
         private int x;
         private int y;
         private Surface toPaint;
+        private static SKPaint ReplacingPaint = new() { BlendMode = SKBlendMode.Src };
         public ImageOperation(int x, int y, Surface image)
         {
             this.x = x;
@@ -14,7 +17,7 @@
 
         public void DrawOnChunk(Chunk chunk, int chunkX, int chunkY)
         {
-            chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, x - chunkX * ChunkPool.ChunkSize, y - chunkY * ChunkPool.ChunkSize);
+            chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, x - chunkX * ChunkPool.ChunkSize, y - chunkY * ChunkPool.ChunkSize, ReplacingPaint);
         }
 
         public HashSet<(int, int)> FindAffectedChunks()

+ 1 - 1
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -3,7 +3,7 @@ using SkiaSharp;
 
 namespace ChunkyImageLib.Operations
 {
-    internal record RectangleOperation : IOperation
+    internal record class RectangleOperation : IOperation
     {
         public RectangleOperation(ShapeData rect)
         {

+ 20 - 21
src/ChunkyImageLib/Surface.cs

@@ -8,58 +8,57 @@ namespace ChunkyImageLib
     {
         private bool disposed;
         private int bytesPerPixel;
-        public SKColorType ColorType { get; }
         private IntPtr PixelBuffer { get; }
         public SKSurface SkiaSurface { get; }
         public int Width { get; }
         public int Height { get; }
 
-        public Surface(int width, int height, SKColorType colorType)
+        public Surface(int width, int height)
         {
-            if (colorType is not SKColorType.RgbaF16 or SKColorType.Bgra8888)
-                throw new ArgumentException("Unsupported color type");
             if (width < 1 || height < 1)
                 throw new ArgumentException("Width and height must be >1");
             if (width > 10000 || height > 1000)
                 throw new ArgumentException("Width and height must be <=10000");
 
-            ColorType = colorType;
-            bytesPerPixel = colorType == SKColorType.RgbaF16 ? 8 : 4;
+            Width = width;
+            Height = height;
+
+            bytesPerPixel = 8;
             PixelBuffer = CreateBuffer(width, height, bytesPerPixel);
             SkiaSurface = CreateSKSurface();
         }
 
-        public Surface(Surface original) : this(original.Width, original.Height, original.ColorType)
+        public Surface(Surface original) : this(original.Width, original.Height)
         {
             SkiaSurface.Canvas.DrawSurface(original.SkiaSurface, 0, 0);
         }
 
         public unsafe void CopyTo(Surface other)
         {
-            if (other.Width != Width || other.Height != Height || other.ColorType != ColorType)
-                throw new ArgumentException("Target ImageData must have the same format");
+            if (other.Width != Width || other.Height != Height)
+                throw new ArgumentException("Target Surface must have the same dimensions");
             int bytesC = Width * Height * bytesPerPixel;
             Buffer.MemoryCopy((void*)PixelBuffer, (void*)other.PixelBuffer, bytesC, bytesC);
         }
 
         public unsafe SKColor GetSRGBPixel(int x, int y)
         {
-            if (ColorType == SKColorType.RgbaF16)
-            {
-                Half* ptr = (Half*)(PixelBuffer + (x + y * Width) * bytesPerPixel);
-                float a = (float)ptr[3];
-                return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
-            }
-            else
-            {
-                // todo later
-                throw new NotImplementedException();
-            }
+            Half* ptr = (Half*)(PixelBuffer + (x + y * Width) * bytesPerPixel);
+            float a = (float)ptr[3];
+            return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
+        }
+
+        public void SaveToDesktop(string filename = "savedSurface.png")
+        {
+            using var snapshot = SkiaSurface.Snapshot();
+            using var stream = File.Create(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), filename));
+            using var png = snapshot.Encode();
+            png.SaveTo(stream);
         }
 
         private SKSurface CreateSKSurface()
         {
-            var surface = SKSurface.Create(new SKImageInfo(Width, Height, ColorType, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), PixelBuffer);
+            var surface = SKSurface.Create(new SKImageInfo(Width, Height, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), PixelBuffer);
             if (surface == null)
                 throw new Exception("Could not create surface");
             return surface;

+ 22 - 6
src/PixiEditorPrototype/Models/ActionAccumulator.cs

@@ -1,6 +1,9 @@
 using ChangeableDocument;
 using ChangeableDocument.Actions;
 using ChangeableDocument.ChangeInfos;
+using PixiEditorPrototype.ViewModels;
+using StructureRenderer;
+using StructureRenderer.RenderInfos;
 using System.Collections.Generic;
 
 namespace PixiEditorPrototype.Models
@@ -8,22 +11,24 @@ namespace PixiEditorPrototype.Models
     internal class ActionAccumulator
     {
         private bool executing = false;
-        private bool applying = false;
 
         private List<IAction> queuedActions = new();
         private DocumentChangeTracker tracker;
+        private DocumentViewModel document;
         private DocumentUpdater documentUpdater;
+        private Renderer renderer;
 
-        public ActionAccumulator(DocumentChangeTracker tracker, DocumentUpdater updater)
+        public ActionAccumulator(DocumentChangeTracker tracker, DocumentUpdater updater, DocumentViewModel document)
         {
             this.tracker = tracker;
             this.documentUpdater = updater;
+            this.document = document;
+
+            renderer = new(tracker);
         }
 
         public void AddAction(IAction action)
         {
-            if (applying)
-                return;
             queuedActions.Add(action);
             TryExecuteAccumulatedActions();
         }
@@ -39,12 +44,23 @@ namespace PixiEditorPrototype.Models
                 queuedActions = new List<IAction>();
 
                 var result = await tracker.ProcessActions(toExecute);
-                applying = true;
+
                 foreach (IChangeInfo? info in result)
                 {
                     documentUpdater.ApplyChangeFromChangeInfo(info);
                 }
-                applying = false;
+
+                document.FinalBitmap.Lock();
+                var renderResult = await renderer.ProcessChanges(result!, document.FinalBitmapSurface, document.FinalBitmap.PixelWidth, document.FinalBitmap.PixelHeight);
+
+                foreach (IRenderInfo info in renderResult)
+                {
+                    if (info is DirtyRect_RenderInfo dirtyRect)
+                    {
+                        document.FinalBitmap.AddDirtyRect(new(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
+                    }
+                }
+                document.FinalBitmap.Unlock();
             }
 
             executing = false;

+ 1 - 0
src/PixiEditorPrototype/PixiEditorPrototype.csproj

@@ -9,6 +9,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
+    <PackageReference Include="PixiEditor.ColorPicker" Version="3.2.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 71 - 1
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -1,8 +1,13 @@
 using ChangeableDocument;
 using ChangeableDocument.Actions;
+using ChangeableDocument.Actions.Drawing;
 using PixiEditorPrototype.Models;
+using SkiaSharp;
 using System.ComponentModel;
 using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
 
 namespace PixiEditorPrototype.ViewModels
 {
@@ -35,13 +40,30 @@ namespace PixiEditorPrototype.ViewModels
         public RelayCommand? DeleteStructureMemberCommand { get; }
         public RelayCommand? ChangeSelectedItemCommand { get; }
 
+        public RelayCommand? MouseDownCommand { get; }
+        public RelayCommand? MouseMoveCommand { get; }
+        public RelayCommand? MouseUpCommand { get; }
+
+        private WriteableBitmap finalBitmap = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null);
+        public WriteableBitmap FinalBitmap
+        {
+            get => finalBitmap;
+            set
+            {
+                finalBitmap = value;
+                PropertyChanged?.Invoke(this, new(nameof(FinalBitmap)));
+            }
+        }
+        public SKSurface FinalBitmapSurface { get; set; }
+
+        public Color SelectedColor { get; set; } = Colors.Black;
 
         public DocumentViewModel()
         {
             Tracker = new DocumentChangeTracker();
             Updater = new DocumentUpdater(this);
             StructureRoot = new FolderViewModel(this, Tracker.Document.ReadOnlyStructureRoot);
-            ActionAccumulator = new ActionAccumulator(Tracker, Updater);
+            ActionAccumulator = new ActionAccumulator(Tracker, Updater, this);
             StructureHelper = new DocumentStructureHelper(this);
 
             UndoCommand = new RelayCommand(Undo, _ => true);
@@ -50,6 +72,54 @@ namespace PixiEditorPrototype.ViewModels
             CreateNewFolderCommand = new RelayCommand(_ => StructureHelper.CreateNewStructureMember(StructureMemberType.Folder), _ => true);
             DeleteStructureMemberCommand = new RelayCommand(DeleteStructureMember, _ => true);
             ChangeSelectedItemCommand = new RelayCommand(ChangeSelectedItem, _ => true);
+
+            MouseDownCommand = new RelayCommand(MouseDown);
+            MouseMoveCommand = new RelayCommand(MouseMove);
+            MouseUpCommand = new RelayCommand(MouseUp);
+
+            FinalBitmapSurface = SKSurface.Create(
+                new SKImageInfo(FinalBitmap.PixelWidth, FinalBitmap.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul),
+                FinalBitmap.BackBuffer,
+                FinalBitmap.BackBufferStride);
+        }
+
+        private bool drawing = false;
+        private int mouseDownX = 0;
+        private int mouseDownY = 0;
+        public void MouseDown(object? param)
+        {
+            if (SelectedStructureMember != null && SelectedStructureMember is LayerViewModel)
+            {
+                drawing = true;
+                var args = (MouseButtonEventArgs)(param!);
+                var source = (System.Windows.Controls.Image)args.Source;
+                var pos = args.GetPosition(source);
+                mouseDownX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
+                mouseDownY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
+            }
+        }
+
+        public void MouseMove(object? param)
+        {
+            if (!drawing)
+                return;
+            var args = (MouseEventArgs)(param!);
+            var source = (System.Windows.Controls.Image)args.Source;
+            var pos = args.GetPosition(source);
+            int curX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
+            int curY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
+            ActionAccumulator.AddAction(
+                new DrawRectangle_Action(
+                    SelectedStructureMember!.GuidValue,
+                    new(mouseDownX, mouseDownY, curX - mouseDownX, curY - mouseDownY, 1, new SKColor(SelectedColor.R, SelectedColor.G, SelectedColor.B, SelectedColor.A), SKColors.Transparent)));
+        }
+
+        public void MouseUp(object? param)
+        {
+            if (!drawing)
+                return;
+            ActionAccumulator.AddAction(new EndDrawRectangle_Action());
+            drawing = false;
         }
 
         public void DeleteStructureMember(object? param)

+ 15 - 1
src/PixiEditorPrototype/Views/DocumentView.xaml

@@ -4,6 +4,7 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:PixiEditorPrototype.Views"
+             xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
              xmlns:behaviors="clr-namespace:PixiEditorPrototype.Behaviors"
              xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
              xmlns:cmd="mvvm"
@@ -70,10 +71,23 @@
             <StackPanel Orientation="Horizontal" Margin="5" Background="White">
                 <Button Width="50" Margin="0,0,5,0" Command="{Binding UndoCommand}">Undo</Button>
                 <Button Width="50" Command="{Binding RedoCommand}">Redo</Button>
+                <colorpicker:PortableColorPicker Margin="5,0,0,0" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="40" Height="20"/>
             </StackPanel>
         </Border>
         <Border BorderThickness="1" Background="Transparent" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5">
-            <Image Margin="5"/>
+            <Image Margin="5" Source="{Binding FinalBitmap}" Width="200" Height="200" RenderOptions.BitmapScalingMode="NearestNeighbor">
+                <i:Interaction.Triggers>
+                    <i:EventTrigger EventName="MouseDown">
+                        <i:InvokeCommandAction Command="{Binding MouseDownCommand}" PassEventArgsToCommand="True"/>
+                    </i:EventTrigger>
+                    <i:EventTrigger EventName="MouseMove">
+                        <i:InvokeCommandAction Command="{Binding MouseMoveCommand}" PassEventArgsToCommand="True"/>
+                    </i:EventTrigger>
+                    <i:EventTrigger EventName="MouseUp">
+                        <i:InvokeCommandAction Command="{Binding MouseUpCommand}" PassEventArgsToCommand="True"/>
+                    </i:EventTrigger>
+                </i:Interaction.Triggers>
+            </Image>
         </Border>
     </DockPanel>
 </UserControl>

+ 18 - 0
src/StructureRenderer/RenderInfos/DirtyRect_RenderInfo.cs

@@ -0,0 +1,18 @@
+namespace StructureRenderer.RenderInfos
+{
+    public record struct DirtyRect_RenderInfo : IRenderInfo
+    {
+        public DirtyRect_RenderInfo(int x, int y, int width, int height)
+        {
+            X = x;
+            Y = y;
+            Width = width;
+            Height = height;
+        }
+
+        public int X { get; }
+        public int Y { get; }
+        public int Width { get; }
+        public int Height { get; }
+    }
+}

+ 6 - 0
src/StructureRenderer/RenderInfos/IRenderInfo.cs

@@ -0,0 +1,6 @@
+namespace StructureRenderer.RenderInfos
+{
+    public interface IRenderInfo
+    {
+    }
+}

+ 108 - 2
src/StructureRenderer/Renderer.cs

@@ -1,19 +1,125 @@
 using ChangeableDocument;
+using ChangeableDocument.Changeables.Interfaces;
 using ChangeableDocument.ChangeInfos;
+using ChunkyImageLib;
+using SkiaSharp;
+using StructureRenderer.RenderInfos;
 
 namespace StructureRenderer
 {
     public class Renderer
     {
         private DocumentChangeTracker tracker;
+        private Surface? backSurface;
+        private static SKPaint BlendingPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private static SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
         public Renderer(DocumentChangeTracker tracker)
         {
             this.tracker = tracker;
         }
 
-        public async Task<List<IChangeInfo>> ProcessChanges(IReadOnlyList<IChangeInfo> changes)
+        public async Task<List<IRenderInfo>> ProcessChanges(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, int screenW, int screenH)
         {
-            throw new NotImplementedException();
+            return await Task.Run(() => Render(changes, screenSurface, screenW, screenH)).ConfigureAwait(true);
+        }
+
+        private HashSet<(int, int)> FindChunksToRerender(IReadOnlyList<IChangeInfo> changes)
+        {
+            HashSet<(int, int)> chunks = new();
+            foreach (var change in changes)
+            {
+                if (change is LayerImageChunks_ChangeInfo layerImageChunks)
+                {
+                    if (layerImageChunks.Chunks == null)
+                        throw new Exception("Chunks must not be null");
+                    chunks.UnionWith(layerImageChunks.Chunks);
+                }
+            }
+            return chunks;
+        }
+
+        private List<IRenderInfo> Render(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, int screenW, int screenH)
+        {
+            bool redrawEverything = false;
+            if (backSurface == null || backSurface.Width != screenW || backSurface.Height != screenH)
+            {
+                backSurface?.Dispose();
+                backSurface = new(screenW, screenH);
+                redrawEverything = true;
+            }
+
+            DirtyRect_RenderInfo? info = null;
+            // draw to back surface
+            if (redrawEverything)
+            {
+                RenderScreen(screenW, screenH, screenSurface);
+                info = new(0, 0, screenW, screenH);
+            }
+            else
+            {
+                HashSet<(int, int)> chunks = FindChunksToRerender(changes);
+                var (minX, minY, maxX, maxY) = (int.MaxValue, int.MaxValue, int.MinValue, int.MinValue);
+                foreach (var (x, y) in chunks)
+                {
+                    RenderChunk(x, y, screenSurface);
+                    (minX, minY) = (Math.Min(x, minX), Math.Min(y, minY));
+                    (maxX, maxY) = (Math.Max(x, maxX), Math.Max(y, maxY));
+                }
+                if (minX != int.MaxValue)
+                {
+                    info = new(
+                        minX * ChunkyImage.ChunkSize,
+                        minY * ChunkyImage.ChunkSize,
+                        (maxX - minX + 1) * ChunkyImage.ChunkSize,
+                        (maxY - minY + 1) * ChunkyImage.ChunkSize);
+                }
+            }
+
+            // transfer back surface to screen surface
+            screenSurface.Canvas.DrawSurface(backSurface.SkiaSurface, 0, 0);
+
+            return info == null ? new() : new() { info };
+        }
+
+        private void RenderScreen(int screenW, int screenH, SKSurface screenSurface)
+        {
+            int chunksWidth = (int)Math.Ceiling(screenW / (float)ChunkyImage.ChunkSize);
+            int chunksHeight = (int)Math.Ceiling(screenH / (float)ChunkyImage.ChunkSize);
+            for (int x = 0; x < chunksWidth; x++)
+            {
+                for (int y = 0; y < chunksHeight; y++)
+                {
+                    RenderChunk(x, y, screenSurface);
+                }
+            }
+        }
+
+        private void RenderChunk(int chunkX, int chunkY, SKSurface screenSurface)
+        {
+            screenSurface.Canvas.DrawRect(chunkX * ChunkyImage.ChunkSize, chunkY * ChunkyImage.ChunkSize, ChunkyImage.ChunkSize, ChunkyImage.ChunkSize, ClearPaint);
+            ForEachLayer((layer) =>
+            {
+                var chunk = layer.LayerImage.GetChunk(chunkX, chunkY);
+                if (chunk == null)
+                    return;
+                using var snapshot = chunk.Snapshot();
+                screenSurface.Canvas.DrawImage(snapshot, chunkX * ChunkyImage.ChunkSize, chunkY * ChunkyImage.ChunkSize, BlendingPaint);
+            }, tracker.Document.ReadOnlyStructureRoot);
+        }
+
+        private void ForEachLayer(Action<IReadOnlyLayer> action, IReadOnlyFolder folder)
+        {
+            foreach (var child in folder.ReadOnlyChildren)
+            {
+                if (child is IReadOnlyLayer layer)
+                {
+                    action(layer);
+                }
+                else if (child is IReadOnlyFolder innerFolder)
+                {
+                    ForEachLayer(action, innerFolder);
+                }
+            }
         }
     }
 }