From d5d953a615a4c4c31617851f87c83cb70a35f610 Mon Sep 17 00:00:00 2001 From: Equbuxu Date: Wed, 22 Feb 2023 13:19:32 +0300 Subject: [PATCH] Cropped layer previews wip --- src/ChunkyImageLib/ChunkyImage.cs | 34 ++++-- .../DataHolders/ChunkResolutionEx.cs | 15 +++ src/ChunkyImageLib/IReadOnlyChunkyImage.cs | 4 +- .../Changeables/Document.cs | 6 +- .../Interfaces/IReadOnlyDocument.cs | 2 +- .../TransformSelectedArea_UpdateableChange.cs | 2 +- .../Changes/Root/CenterContent_Change.cs | 2 +- .../Changes/Root/ClipCanvas_Change.cs | 2 +- .../Changes/Root/FlipImage_Change.cs | 2 +- .../Changes/Root/RotateImage_Change.cs | 2 +- .../Numerics/RectI.cs | 5 + .../Models/Rendering/MemberPreviewUpdater.cs | 101 +++++++++++++++--- .../DocumentViewModel.Serialization.cs | 4 +- .../Document/DocumentViewModel.cs | 2 +- 14 files changed, 150 insertions(+), 33 deletions(-) diff --git a/src/ChunkyImageLib/ChunkyImage.cs b/src/ChunkyImageLib/ChunkyImage.cs index 9c505332c..db0a24a84 100644 --- a/src/ChunkyImageLib/ChunkyImage.cs +++ b/src/ChunkyImageLib/ChunkyImage.cs @@ -37,7 +37,7 @@ namespace ChunkyImageLib; /// - BlendMode.Src: default mode, the latest chunks are the same as committed ones but with some or all queued operations applied. /// This means that operations can work with the existing pixels. /// - Any other blend mode: the latest chunks contain only the things drawn by the queued operations. -/// They need to be drawn over the committed chunks to obtain the final image. In this case, operations won't have access to the existing pixels. +/// They need to be drawn over the committed chunks to obtain the final image. In this case, operations won't have access to the existing pixels. /// public class ChunkyImage : IReadOnlyChunkyImage, IDisposable { @@ -117,7 +117,7 @@ public ChunkyImage(VecI size) } /// This image is disposed - public RectI? FindLatestBounds() + public RectI? FindChunkAlignedMostUpToDateBounds() { lock (lockObject) { @@ -129,7 +129,27 @@ public ChunkyImage(VecI size) rect ??= chunkBounds; rect = rect.Value.Union(chunkBounds); } - foreach (var (pos, _) in latestChunks[ChunkResolution.Full]) + foreach (var operation in queuedOperations) + { + foreach (var pos in operation.affectedArea.Chunks) + { + RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize)); + rect ??= chunkBounds; + rect = rect.Value.Union(chunkBounds); + } + } + return rect; + } + } + + /// This image is disposed + public RectI? FindChunkAlignedCommittedBounds() + { + lock (lockObject) + { + ThrowIfDisposed(); + RectI? rect = null; + foreach (var (pos, _) in committedChunks[ChunkResolution.Full]) { RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize)); rect ??= chunkBounds; @@ -140,23 +160,25 @@ public ChunkyImage(VecI size) } /// This image is disposed - public RectI? FindPreciseCommittedBounds() + public RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full) { lock (lockObject) { ThrowIfDisposed(); + var chunkSize = precision.PixelSize(); RectI? preciseBounds = null; - foreach (var (chunkPos, chunk) in committedChunks[ChunkResolution.Full]) + foreach (var (chunkPos, chunk) in committedChunks[precision]) { RectI? chunkPreciseBounds = chunk.FindPreciseBounds(); if(chunkPreciseBounds is null) continue; - RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * FullChunkSize); + RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize); preciseBounds ??= globalChunkBounds; preciseBounds = preciseBounds.Value.Union(globalChunkBounds); } + preciseBounds = (RectI?)preciseBounds?.Scale(precision.InvertedMultiplier()).RoundOutwards(); preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize)); return preciseBounds; diff --git a/src/ChunkyImageLib/DataHolders/ChunkResolutionEx.cs b/src/ChunkyImageLib/DataHolders/ChunkResolutionEx.cs index 02ac6e139..86a024dce 100644 --- a/src/ChunkyImageLib/DataHolders/ChunkResolutionEx.cs +++ b/src/ChunkyImageLib/DataHolders/ChunkResolutionEx.cs @@ -17,6 +17,21 @@ public static double Multiplier(this ChunkResolution resolution) }; } + /// + /// Returns the inverted multiplier of the . + /// + public static double InvertedMultiplier(this ChunkResolution resolution) + { + return resolution switch + { + ChunkResolution.Full => 1, + ChunkResolution.Half => 2, + ChunkResolution.Quarter => 4, + ChunkResolution.Eighth => 8, + _ => 1, + }; + } + /// /// Returns the size of a chunk of the resolution /// diff --git a/src/ChunkyImageLib/IReadOnlyChunkyImage.cs b/src/ChunkyImageLib/IReadOnlyChunkyImage.cs index 44e547024..48ad93dd3 100644 --- a/src/ChunkyImageLib/IReadOnlyChunkyImage.cs +++ b/src/ChunkyImageLib/IReadOnlyChunkyImage.cs @@ -10,7 +10,9 @@ public interface IReadOnlyChunkyImage { bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null); bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null); - RectI? FindLatestBounds(); + RectI? FindChunkAlignedMostUpToDateBounds(); + RectI? FindChunkAlignedCommittedBounds(); + RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full); Color GetCommittedPixel(VecI posOnImage); Color GetMostUpToDatePixel(VecI posOnImage); bool LatestOrCommittedChunkExists(VecI chunkPos); diff --git a/src/PixiEditor.ChangeableDocument/Changeables/Document.cs b/src/PixiEditor.ChangeableDocument/Changeables/Document.cs index 01897a880..03a8b8df0 100644 --- a/src/PixiEditor.ChangeableDocument/Changeables/Document.cs +++ b/src/PixiEditor.ChangeableDocument/Changeables/Document.cs @@ -52,7 +52,7 @@ public void Dispose() throw new ArgumentException(@"The given guid does not belong to a layer.", nameof(layerGuid)); - RectI? tightBounds = layer.LayerImage.FindLatestBounds(); + RectI? tightBounds = layer.LayerImage.FindChunkAlignedMostUpToDateBounds(); if (tightBounds is null) return null; @@ -69,7 +69,7 @@ public void Dispose() return surface; } - public RectI? GetLayerTightBounds(Guid layerGuid) + public RectI? GetChunkAlignedLayerBounds(Guid layerGuid) { var layer = (IReadOnlyLayer?)FindMember(layerGuid); @@ -77,7 +77,7 @@ public void Dispose() throw new ArgumentException(@"The given guid does not belong to a layer.", nameof(layerGuid)); - return layer.LayerImage.FindLatestBounds(); + return layer.LayerImage.FindChunkAlignedMostUpToDateBounds(); } public void ForEveryReadonlyMember(Action action) => ForEveryReadonlyMember(StructureRoot, action); diff --git a/src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs b/src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs index 4a1a50c41..3fa7f647d 100644 --- a/src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs +++ b/src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs @@ -46,7 +46,7 @@ public interface IReadOnlyDocument void ForEveryReadonlyMember(Action action); public Surface? GetLayerImage(Guid layerGuid); - public RectI? GetLayerTightBounds(Guid layerGuid); + public RectI? GetChunkAlignedLayerBounds(Guid layerGuid); /// /// Finds the member with the or returns null if not found diff --git a/src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs b/src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs index 298bfc0b7..c0e4f1539 100644 --- a/src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs +++ b/src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs @@ -72,7 +72,7 @@ public override bool InitializeAndValidate(Document target) public OneOf ExtractArea(ChunkyImage image, VectorPath path, RectI pathBounds) { // get rid of transparent areas on edges - var memberImageBounds = image.FindLatestBounds(); + var memberImageBounds = image.FindChunkAlignedMostUpToDateBounds(); if (memberImageBounds is null) return new None(); pathBounds = pathBounds.Intersect(memberImageBounds.Value); diff --git a/src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs b/src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs index ee17d0749..55222362c 100644 --- a/src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs +++ b/src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs @@ -41,7 +41,7 @@ private VecI CalculateCurrentOffset(Document document) foreach (var layerGuid in affectedLayers) { Layer layer = document.FindMemberOrThrow(layerGuid); - RectI? tightBounds = layer.LayerImage.FindPreciseCommittedBounds(); + RectI? tightBounds = layer.LayerImage.FindTightCommittedBounds(); if (tightBounds.HasValue) { currentBounds = currentBounds.HasValue ? currentBounds.Value.Union(tightBounds.Value) : tightBounds; diff --git a/src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs b/src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs index a37a48a6b..ed3ca868a 100644 --- a/src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs +++ b/src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs @@ -15,7 +15,7 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase { if (member is Layer layer) { - var layerBounds = layer.LayerImage.FindPreciseCommittedBounds(); + var layerBounds = layer.LayerImage.FindTightCommittedBounds(); if (layerBounds.HasValue) { bounds ??= layerBounds.Value; diff --git a/src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs b/src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs index cf0022331..f91dc1686 100644 --- a/src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs +++ b/src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs @@ -54,7 +54,7 @@ private void FlipImage(ChunkyImage img) RectI bounds = new RectI(VecI.Zero, img.LatestSize); if (membersToFlip.Count > 0) { - var preciseBounds = img.FindPreciseCommittedBounds(); + var preciseBounds = img.FindTightCommittedBounds(); if (preciseBounds.HasValue) { bounds = preciseBounds.Value; diff --git a/src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs b/src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs index 9f8baf833..348ea504f 100644 --- a/src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs +++ b/src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs @@ -59,7 +59,7 @@ public override bool InitializeAndValidate(Document target) RectI bounds = new RectI(VecI.Zero, img.CommittedSize); if (membersToRotate.Count > 0) { - var preciseBounds = img.FindPreciseCommittedBounds(); + var preciseBounds = img.FindTightCommittedBounds(); if (preciseBounds.HasValue) { bounds = preciseBounds.Value; diff --git a/src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs b/src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs index 74a3a0727..d4c387208 100644 --- a/src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs +++ b/src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs @@ -261,6 +261,11 @@ public static RectI FromTwoPixels(VecI pixel, VecI oppositePixel) return x > left && x < right && y > top && y < bottom; } + public readonly bool ContainsExclusive(RectI rect) + { + return ContainsExclusive(rect.TopLeft) && ContainsExclusive(rect.BottomRight); + } + public readonly bool ContainsPixel(VecI pixelTopLeft) => ContainsPixel(pixelTopLeft.X, pixelTopLeft.Y); public readonly bool ContainsPixel(int pixelTopLeftX, int pixelTopLeftY) { diff --git a/src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs b/src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs index 5b54ced2d..6aae3187c 100644 --- a/src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs +++ b/src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs @@ -13,6 +13,8 @@ using PixiEditor.Models.DocumentModels; using PixiEditor.Models.Rendering.RenderInfos; using PixiEditor.ViewModels.SubViewModels.Document; +using System.Diagnostics; +using System.Drawing.Text; namespace PixiEditor.Models.Rendering; internal class MemberPreviewUpdater @@ -20,6 +22,7 @@ internal class MemberPreviewUpdater private readonly DocumentViewModel doc; private readonly DocumentInternalParts internals; + private Dictionary lastTightBounds = new(); private Dictionary previewDelayedAreas = new(); private Dictionary maskPreviewDelayedAreas = new(); @@ -52,15 +55,21 @@ public List UpdateGatheredChunksSync private List Render(AffectedAreasGatherer chunkGatherer, bool rerenderPreviews) { + Stopwatch sw = Stopwatch.StartNew(); List infos = new(); var (imagePreviewChunksToRerender, maskPreviewChunksToRerender) = FindPreviewChunksToRerender(chunkGatherer, !rerenderPreviews); var previewSize = StructureMemberViewModel.CalculatePreviewSize(internals.Tracker.Document.Size); float scaling = (float)previewSize.X / doc.SizeBindable.X; UpdateImagePreviews(imagePreviewChunksToRerender, scaling, infos); + if (rerenderPreviews) + Trace.WriteLine("image" + (sw.ElapsedTicks * 1000 / (double)Stopwatch.Frequency).ToString()); UpdateMaskPreviews(maskPreviewChunksToRerender, scaling, infos); - return infos; + if (rerenderPreviews) + Trace.WriteLine(sw.ElapsedTicks * 1000 / (double)Stopwatch.Frequency ); + sw.Stop(); + return infos; } private static void AddAreas(Dictionary from, Dictionary to) @@ -137,6 +146,77 @@ private void UpdateWholeCanvasPreview(Dictionary imagePrevie infos.Add(new CanvasPreviewDirty_RenderInfo()); } + private RectI? FindLayerTightBounds(IReadOnlyLayer layer) + { + // premature optimization here we go + RectI? bounds = layer.LayerImage.FindChunkAlignedCommittedBounds(); + if (bounds is null) + return null; + + int biggest = bounds.Value.Size.LongestAxis; + ChunkResolution resolution = biggest switch + { + > 2048 => ChunkResolution.Eighth, + > 1024 => ChunkResolution.Quarter, + > 512 => ChunkResolution.Half, + _ => ChunkResolution.Full, + }; + return layer.LayerImage.FindTightCommittedBounds(resolution); + } + + private void UpdateLayerPreviewSurface(IReadOnlyLayer layer, StructureMemberViewModel memberVM, AffectedArea area, float scaling) + { + RectI? prevTightBounds = null; + if (lastTightBounds.TryGetValue(layer.GuidValue, out RectI tightBounds)) + prevTightBounds = tightBounds; + + RectI? newTightBounds; + + if (prevTightBounds is null) + { + newTightBounds = FindLayerTightBounds(layer); + } + else if (prevTightBounds.Value.ContainsExclusive(area.GlobalArea.Value)) + { + // if the affected area is fully inside the previous tight bounds, the tight bounds couldn't possibly have changed + newTightBounds = prevTightBounds.Value; + } + else + { + newTightBounds = FindLayerTightBounds(layer); + } + + if (newTightBounds is null) + { + memberVM.PreviewSurface.Canvas.Clear(); + return; + } + + if (newTightBounds == prevTightBounds) + { + memberVM.PreviewSurface.Canvas.Save(); + memberVM.PreviewSurface.Canvas.Scale(scaling); + memberVM.PreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea); + + foreach (var chunk in area.Chunks) + { + var pos = chunk * ChunkResolution.Full.PixelSize(); + if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, SmoothReplacingPaint)) + memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint); + } + + memberVM.PreviewSurface.Canvas.Restore(); + return; + } + + int biggestAxis = newTightBounds.Value.Size.LongestAxis; + RectI targetBounds = (RectI)RectD.FromCenterAndSize(newTightBounds.Value.Center, new(biggestAxis)).RoundOutwards(); + + memberVM.PreviewSurface.Canvas.Save(); + memberVM.PreviewSurface.Canvas.Scale(scaling); + memberVM.PreviewSurface.Canvas.Scale() + } + private void UpdateMembersImagePreviews(Dictionary imagePreviewChunks, float scaling, List infos) { foreach (var (guid, area) in imagePreviewChunks) @@ -148,24 +228,17 @@ private void UpdateMembersImagePreviews(Dictionary imagePrev continue; var member = internals.Tracker.Document.FindMemberOrThrow(guid); - memberVM.PreviewSurface.Canvas.Save(); - memberVM.PreviewSurface.Canvas.Scale(scaling); - memberVM.PreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea); + if (memberVM is LayerViewModel) { - var layer = (IReadOnlyLayer)member; - foreach (var chunk in area.Chunks) - { - 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)) - memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint); - } + UpdateLayerPreviewSurface((IReadOnlyLayer)member, memberVM, area, scaling); infos.Add(new PreviewDirty_RenderInfo(guid)); } else if (memberVM is FolderViewModel) { + memberVM.PreviewSurface.Canvas.Save(); + memberVM.PreviewSurface.Canvas.Scale(scaling); + memberVM.PreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea); var folder = (IReadOnlyFolder)member; foreach (var chunk in area.Chunks) { @@ -183,9 +256,9 @@ private void UpdateMembersImagePreviews(Dictionary imagePrev memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(), ChunkResolution.Full.PixelSize(), ClearPaint); } } + memberVM.PreviewSurface.Canvas.Restore(); infos.Add(new PreviewDirty_RenderInfo(guid)); } - memberVM.PreviewSurface.Canvas.Restore(); } } diff --git a/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs b/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs index 6a592cc7f..92cee2cc8 100644 --- a/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs +++ b/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs @@ -110,7 +110,7 @@ private static ImageLayer ToSerializable(IReadOnlyLayer layer, IReadOnlyDocument { var result = document.GetLayerImage(layer.GuidValue); - var tightBounds = document.GetLayerTightBounds(layer.GuidValue); + var tightBounds = document.GetChunkAlignedLayerBounds(layer.GuidValue); using var data = result?.DrawingSurface.Snapshot().Encode(); byte[] bytes = data?.AsSpan().ToArray(); var serializable = new ImageLayer @@ -130,7 +130,7 @@ private static Mask GetMask(IReadOnlyChunkyImage mask, bool maskVisible) if (mask == null) return null; - var maskBound = mask.FindLatestBounds(); + var maskBound = mask.FindChunkAlignedMostUpToDateBounds(); if (maskBound == null) { diff --git a/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs b/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs index 624fee4e8..e03c57d90 100644 --- a/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs +++ b/src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs @@ -348,7 +348,7 @@ public void MarkAsUnsaved() RectI? memberImageBounds; try { - memberImageBounds = layer.LayerImage.FindLatestBounds(); + memberImageBounds = layer.LayerImage.FindChunkAlignedMostUpToDateBounds(); } catch (ObjectDisposedException) {