|
@@ -0,0 +1,1142 @@
|
|
|
+using System.Runtime.CompilerServices;
|
|
|
+using ChunkyImageLib.DataHolders;
|
|
|
+using ChunkyImageLib.Operations;
|
|
|
+using OneOf;
|
|
|
+using OneOf.Types;
|
|
|
+using SkiaSharp;
|
|
|
+
|
|
|
+[assembly: InternalsVisibleTo("ChunkyImageLibTest")]
|
|
|
+
|
|
|
+namespace ChunkyImageLib;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// This class is thread-safe only for reading! Only the functions from IReadOnlyChunkyImage can be called from any thread.
|
|
|
+/// ChunkyImage can be in two general states:
|
|
|
+/// 1. a state with all chunks committed and no queued operations
|
|
|
+/// - latestChunks and latestChunksData are empty
|
|
|
+/// - queuedOperations are empty
|
|
|
+/// - committedChunks[ChunkResolution.Full] contains the current versions of all stored chunks
|
|
|
+/// - committedChunks[*any other resolution*] may contain the current low res versions of some of the chunks (or all of them, or none)
|
|
|
+/// - LatestSize == CommittedSize == current image size (px)
|
|
|
+/// 2. and a state with some queued operations
|
|
|
+/// - queuedOperations contains all requested operations (drawing, raster clips, clear, etc.)
|
|
|
+/// - committedChunks[ChunkResolution.Full] contains the last versions before any operations of all stored chunks
|
|
|
+/// - committedChunks[*any other resolution*] may contain the last low res versions before any operations of some of the chunks (or all of them, or none)
|
|
|
+/// - latestChunks stores chunks with some (or none, or all) queued operations applied
|
|
|
+/// - latestChunksData stores the data for some or all of the latest chunks (not necessarily synced with latestChunks).
|
|
|
+/// The data includes how many operations from the queue have already been applied to the chunk, as well as chunk deleted state (the clear operation deletes chunks)
|
|
|
+/// - LatestSize contains the new size if any resize operations were requested, otherwise the committed size
|
|
|
+/// You can check the current state via queuedOperations.Count == 0
|
|
|
+///
|
|
|
+/// Depending on the chosen blend mode the latest chunks contain different things:
|
|
|
+/// - SKBlendMode.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.
|
|
|
+/// </summary>
|
|
|
+public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
|
+{
|
|
|
+ private struct LatestChunkData
|
|
|
+ {
|
|
|
+ public LatestChunkData()
|
|
|
+ {
|
|
|
+ QueueProgress = 0;
|
|
|
+ IsDeleted = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int QueueProgress { get; set; }
|
|
|
+ public bool IsDeleted { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool disposed = false;
|
|
|
+ private readonly object lockObject = new();
|
|
|
+ private int commitCounter = 0;
|
|
|
+
|
|
|
+ public const int FullChunkSize = ChunkPool.FullChunkSize;
|
|
|
+ private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
|
|
|
+ private static SKPaint InverseClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstOut };
|
|
|
+ private static SKPaint ReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src };
|
|
|
+ private static SKPaint SmoothReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.Medium };
|
|
|
+ private static SKPaint AddingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Plus };
|
|
|
+ private readonly SKPaint blendModePaint = new SKPaint() { BlendMode = SKBlendMode.Src };
|
|
|
+
|
|
|
+ public VecI CommittedSize { get; private set; }
|
|
|
+ public VecI LatestSize { get; private set; }
|
|
|
+
|
|
|
+ public int QueueLength
|
|
|
+ {
|
|
|
+ get
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ return queuedOperations.Count;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private readonly List<(IOperation operation, HashSet<VecI> affectedChunks)> queuedOperations = new();
|
|
|
+ private readonly List<ChunkyImage> activeClips = new();
|
|
|
+ private SKBlendMode blendMode = SKBlendMode.Src;
|
|
|
+ private bool lockTransparency = false;
|
|
|
+ private SKPath? clippingPath;
|
|
|
+ private int? horizontalSymmetryAxis = null;
|
|
|
+ private int? verticalSymmetryAxis = null;
|
|
|
+
|
|
|
+ private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
|
|
|
+ private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
|
|
|
+ private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
|
|
|
+
|
|
|
+ public ChunkyImage(VecI size)
|
|
|
+ {
|
|
|
+ CommittedSize = size;
|
|
|
+ LatestSize = size;
|
|
|
+ committedChunks = new()
|
|
|
+ {
|
|
|
+ [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new(),
|
|
|
+ };
|
|
|
+ latestChunks = new()
|
|
|
+ {
|
|
|
+ [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new(),
|
|
|
+ };
|
|
|
+ latestChunksData = new()
|
|
|
+ {
|
|
|
+ [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new(),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public ChunkyImage CloneFromCommitted()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ChunkyImage output = new(LatestSize);
|
|
|
+ var chunks = FindCommittedChunks();
|
|
|
+ foreach (var chunk in chunks)
|
|
|
+ {
|
|
|
+ var image = GetCommittedChunk(chunk, ChunkResolution.Full);
|
|
|
+ if (image is null)
|
|
|
+ continue;
|
|
|
+ output.EnqueueDrawImage(chunk * FullChunkSize, image.Surface);
|
|
|
+ }
|
|
|
+
|
|
|
+ output.CommitChanges();
|
|
|
+ return output;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public SKColor GetCommittedPixel(VecI posOnImage)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
|
|
|
+ var posInChunk = posOnImage - chunkPos * FullChunkSize;
|
|
|
+ return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
|
|
|
+ {
|
|
|
+ null => SKColors.Transparent,
|
|
|
+ var chunk => chunk.Surface.GetSRGBPixel(posInChunk)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public SKColor GetMostUpToDatePixel(VecI posOnImage)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
|
|
|
+ var posInChunk = posOnImage - chunkPos * FullChunkSize;
|
|
|
+
|
|
|
+ // nothing queued, return committed
|
|
|
+ if (queuedOperations.Count == 0)
|
|
|
+ {
|
|
|
+ Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ return committedChunk switch
|
|
|
+ {
|
|
|
+ null => SKColors.Transparent,
|
|
|
+ _ => committedChunk.Surface.GetSRGBPixel(posInChunk)
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // something is queued, blend mode is Src so no merging needed
|
|
|
+ if (blendMode == SKBlendMode.Src)
|
|
|
+ {
|
|
|
+ Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ return latestChunk switch
|
|
|
+ {
|
|
|
+ null => SKColors.Transparent,
|
|
|
+ _ => latestChunk.Surface.GetSRGBPixel(posInChunk)
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // something is queued, blend mode is not Src so we have to do merging
|
|
|
+ {
|
|
|
+ Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ SKColor committedColor = committedChunk is null ?
|
|
|
+ SKColors.Transparent :
|
|
|
+ committedChunk.Surface.GetSRGBPixel(posInChunk);
|
|
|
+ SKColor latestColor = latestChunk is null ?
|
|
|
+ SKColors.Transparent :
|
|
|
+ latestChunk.Surface.GetSRGBPixel(posInChunk);
|
|
|
+ // using a whole chunk just to draw 1 pixel is kinda dumb,
|
|
|
+ // but this should be faster than any approach that requires allocations
|
|
|
+ using Chunk tempChunk = Chunk.Create(ChunkResolution.Eighth);
|
|
|
+ using SKPaint committedPaint = new SKPaint() { Color = committedColor, BlendMode = SKBlendMode.Src };
|
|
|
+ using SKPaint latestPaint = new SKPaint() { Color = latestColor, BlendMode = this.blendMode };
|
|
|
+ tempChunk.Surface.SkiaSurface.Canvas.DrawPoint(VecI.Zero, committedPaint);
|
|
|
+ tempChunk.Surface.SkiaSurface.Canvas.DrawPoint(VecI.Zero, latestPaint);
|
|
|
+ return tempChunk.Surface.GetSRGBPixel(VecI.Zero);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <returns>
|
|
|
+ /// True if the chunk existed and was drawn, otherwise false
|
|
|
+ /// </returns>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, SKSurface surface, VecI pos, SKPaint? paint = null)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ OneOf<None, EmptyChunk, Chunk> latestChunk;
|
|
|
+ {
|
|
|
+ var chunk = GetLatestChunk(chunkPos, resolution);
|
|
|
+ if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
|
|
|
+ {
|
|
|
+ latestChunk = new EmptyChunk();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ latestChunk = chunk is null ? new None() : chunk;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var committedChunk = GetCommittedChunk(chunkPos, resolution);
|
|
|
+
|
|
|
+ // draw committed directly
|
|
|
+ if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != SKBlendMode.Src)
|
|
|
+ {
|
|
|
+ if (committedChunk is null)
|
|
|
+ return false;
|
|
|
+ committedChunk.DrawOnSurface(surface, pos, paint);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // no need to combine with committed, draw directly
|
|
|
+ if (blendMode == SKBlendMode.Src || committedChunk is null)
|
|
|
+ {
|
|
|
+ if (latestChunk.IsT2)
|
|
|
+ {
|
|
|
+ latestChunk.AsT2.DrawOnSurface(surface, pos, paint);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // combine with committed and then draw
|
|
|
+ using var tempChunk = Chunk.Create(resolution);
|
|
|
+ tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(committedChunk.Surface.SkiaSurface, 0, 0, ReplacingPaint);
|
|
|
+ blendModePaint.BlendMode = blendMode;
|
|
|
+ tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.SkiaSurface, 0, 0, blendModePaint);
|
|
|
+ if (lockTransparency)
|
|
|
+ OperationHelper.ClampAlpha(tempChunk.Surface.SkiaSurface, committedChunk.Surface.SkiaSurface);
|
|
|
+ tempChunk.DrawOnSurface(surface, pos, paint);
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public bool LatestOrCommittedChunkExists(VecI chunkPos)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
|
|
|
+ MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
|
|
|
+ return true;
|
|
|
+ foreach (var operation in queuedOperations)
|
|
|
+ {
|
|
|
+ if (operation.affectedChunks.Contains(chunkPos))
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ internal bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, SKSurface surface, VecI pos, SKPaint? paint = null)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ var chunk = GetCommittedChunk(chunkPos, resolution);
|
|
|
+ if (chunk is null)
|
|
|
+ return false;
|
|
|
+ chunk.DrawOnSurface(surface, pos, paint);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ internal bool CommittedChunkExists(VecI chunkPos)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Returns the latest version of the chunk if it exists or should exist based on queued operation. The returned chunk is fully up to date.
|
|
|
+ /// </summary>
|
|
|
+ private Chunk? GetLatestChunk(VecI pos, ChunkResolution resolution)
|
|
|
+ {
|
|
|
+ if (queuedOperations.Count == 0)
|
|
|
+ return null;
|
|
|
+
|
|
|
+ MaybeCreateAndProcessQueueForChunk(pos, resolution);
|
|
|
+ var maybeNewlyProcessedChunk = MaybeGetLatestChunk(pos, resolution);
|
|
|
+ return maybeNewlyProcessedChunk;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Tries it's best to return a committed chunk, either if it exists or if it can be created from it's high res version. Returns null if it can't.
|
|
|
+ /// </summary>
|
|
|
+ private Chunk? GetCommittedChunk(VecI pos, ChunkResolution resolution)
|
|
|
+ {
|
|
|
+ var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
|
|
|
+ if (maybeSameRes is not null)
|
|
|
+ return maybeSameRes;
|
|
|
+
|
|
|
+ var maybeFullRes = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
|
|
|
+ if (maybeFullRes is not null)
|
|
|
+ return GetOrCreateCommittedChunk(pos, resolution);
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Chunk? MaybeGetLatestChunk(VecI pos, ChunkResolution resolution)
|
|
|
+ => latestChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
|
|
|
+
|
|
|
+ private Chunk? MaybeGetCommittedChunk(VecI pos, ChunkResolution resolution)
|
|
|
+ => committedChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void AddRasterClip(ChunkyImage clippingMask)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (queuedOperations.Count > 0)
|
|
|
+ throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
+ activeClips.Add(clippingMask);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void SetClippingPath(SKPath clippingPath)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (queuedOperations.Count > 0)
|
|
|
+ throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
+ this.clippingPath = clippingPath;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Porter duff compositing operators (apart from SrcOver) likely won't have the intended effect.
|
|
|
+ /// </summary>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void SetBlendMode(SKBlendMode mode)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (queuedOperations.Count > 0)
|
|
|
+ throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
+ blendMode = mode;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void SetHorizontalAxisOfSymmetry(int position)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (queuedOperations.Count > 0)
|
|
|
+ throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
+ horizontalSymmetryAxis = position;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void SetVerticalAxisOfSymmetry(int position)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (queuedOperations.Count > 0)
|
|
|
+ throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
+ verticalSymmetryAxis = position;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnableLockTransparency()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ lockTransparency = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawRectangle(ShapeData rect)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ RectangleOperation operation = new(rect);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawEllipse(RectI location, SKColor strokeColor, SKColor fillColor, int strokeWidth, SKPaint? paint = null)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, paint);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
|
|
|
+ /// It will however copy the surface right away which can be slow (in updateable changes especially).
|
|
|
+ /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
|
|
|
+ /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
|
|
|
+ /// </summary>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawImage(ShapeCorners corners, Surface image, SKPaint? paint = null, bool copyImage = true)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ImageOperation operation = new(corners, image, paint, copyImage);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
|
|
|
+ /// It will however copy the surface right away which can be slow (in updateable changes especially).
|
|
|
+ /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
|
|
|
+ /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
|
|
|
+ /// </summary>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawImage(VecI pos, Surface image, SKPaint? paint = null, bool copyImage = true)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ImageOperation operation = new(pos, image, paint, copyImage);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawPath(SKPath path, SKColor color, float strokeWidth, SKStrokeCap strokeCap, SKBlendMode blendMode, RectI? customBounds = null)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ PathOperation operation = new(path, color, strokeWidth, strokeCap, blendMode, customBounds);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawBresenhamLine(VecI from, VecI to, SKColor color, SKBlendMode blendMode)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ BresenhamLineOperation operation = new(from, to, color, blendMode);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawSkiaLine(VecI from, VecI to, SKStrokeCap strokeCap, float strokeWidth, SKColor color, SKBlendMode blendMode)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ SkiaLineOperation operation = new(from, to, strokeCap, strokeWidth, color, blendMode);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawPixels(IEnumerable<VecI> pixels, SKColor color, SKBlendMode blendMode)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ PixelsOperation operation = new(pixels, color, blendMode);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawPixel(VecI pos, SKColor color, SKBlendMode blendMode)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ PixelOperation operation = new(pos, color, blendMode);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ChunkyImageOperation operation = new(image, pos, flipHor, flipVer);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueClearRegion(RectI region)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ClearRegionOperation operation = new(region);
|
|
|
+ EnqueueOperation(operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueClear()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ClearOperation operation = new();
|
|
|
+ EnqueueOperation(operation, FindAllChunks());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void EnqueueResize(VecI newSize)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ ResizeOperation operation = new(newSize);
|
|
|
+ LatestSize = newSize;
|
|
|
+ EnqueueOperation(operation, FindAllChunksOutsideBounds(newSize));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void EnqueueOperation(IDrawOperation operation)
|
|
|
+ {
|
|
|
+ List<IDrawOperation> operations = new(4) { operation };
|
|
|
+
|
|
|
+ if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
|
|
|
+ operations.Add(operation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
|
|
|
+ if (horizontalSymmetryAxis is not null)
|
|
|
+ operations.Add(operation.AsMirrored(null, horizontalSymmetryAxis));
|
|
|
+ if (verticalSymmetryAxis is not null)
|
|
|
+ operations.Add(operation.AsMirrored(verticalSymmetryAxis, null));
|
|
|
+
|
|
|
+ foreach (var op in operations)
|
|
|
+ {
|
|
|
+ var chunks = op.FindAffectedChunks();
|
|
|
+ chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
|
|
|
+ if (operation.IgnoreEmptyChunks)
|
|
|
+ chunks.IntersectWith(FindAllChunks());
|
|
|
+ EnqueueOperation(op, chunks);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void EnqueueOperation(IOperation operation, HashSet<VecI> chunks)
|
|
|
+ {
|
|
|
+ queuedOperations.Add((operation, chunks));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void CancelChanges()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ //clear queued operations
|
|
|
+ foreach (var operation in queuedOperations)
|
|
|
+ operation.operation.Dispose();
|
|
|
+ queuedOperations.Clear();
|
|
|
+
|
|
|
+ //clear additional state
|
|
|
+ activeClips.Clear();
|
|
|
+ blendMode = SKBlendMode.Src;
|
|
|
+ lockTransparency = false;
|
|
|
+ horizontalSymmetryAxis = null;
|
|
|
+ verticalSymmetryAxis = null;
|
|
|
+ clippingPath = null;
|
|
|
+
|
|
|
+ //clear latest chunks
|
|
|
+ foreach (var (_, chunksOfRes) in latestChunks)
|
|
|
+ {
|
|
|
+ foreach (var (_, chunk) in chunksOfRes)
|
|
|
+ {
|
|
|
+ chunk.Dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LatestSize = CommittedSize;
|
|
|
+ foreach (var (res, chunks) in latestChunks)
|
|
|
+ {
|
|
|
+ chunks.Clear();
|
|
|
+ latestChunksData[res].Clear();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public void CommitChanges()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ var affectedChunks = FindAffectedChunks();
|
|
|
+ foreach (var chunk in affectedChunks)
|
|
|
+ {
|
|
|
+ MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var (operation, _) in queuedOperations)
|
|
|
+ {
|
|
|
+ operation.Dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ CommitLatestChunks();
|
|
|
+ CommittedSize = LatestSize;
|
|
|
+ queuedOperations.Clear();
|
|
|
+ activeClips.Clear();
|
|
|
+ blendMode = SKBlendMode.Src;
|
|
|
+ lockTransparency = false;
|
|
|
+ horizontalSymmetryAxis = null;
|
|
|
+ verticalSymmetryAxis = null;
|
|
|
+ clippingPath = null;
|
|
|
+
|
|
|
+ commitCounter++;
|
|
|
+ if (commitCounter % 30 == 0)
|
|
|
+ FindAndDeleteEmptyCommittedChunks();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Does all necessary steps to convert latest chunks into committed ones. The latest chunk dictionary become empty after this function is called.
|
|
|
+ /// </summary>
|
|
|
+ private void CommitLatestChunks()
|
|
|
+ {
|
|
|
+ // move/draw fully processed latest chunks to/on committed
|
|
|
+ foreach (var (resolution, chunks) in latestChunks)
|
|
|
+ {
|
|
|
+ foreach (var (pos, chunk) in chunks)
|
|
|
+ {
|
|
|
+ // get chunk if exists
|
|
|
+ LatestChunkData data = latestChunksData[resolution][pos];
|
|
|
+ if (data.QueueProgress != queuedOperations.Count)
|
|
|
+ {
|
|
|
+ if (resolution == ChunkResolution.Full)
|
|
|
+ {
|
|
|
+ throw new InvalidOperationException("Trying to commit a full res chunk that wasn't fully processed");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ chunk.Dispose();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // do a swap
|
|
|
+ if (blendMode == SKBlendMode.Src)
|
|
|
+ {
|
|
|
+ // delete committed version
|
|
|
+ if (committedChunks[resolution].ContainsKey(pos))
|
|
|
+ {
|
|
|
+ var oldChunk = committedChunks[resolution][pos];
|
|
|
+ committedChunks[resolution].Remove(pos);
|
|
|
+ oldChunk.Dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ // put the latest version in place of the committed one
|
|
|
+ if (!data.IsDeleted)
|
|
|
+ committedChunks[resolution].Add(pos, chunk);
|
|
|
+ else
|
|
|
+ chunk.Dispose();
|
|
|
+ }
|
|
|
+ // do blending
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // nothing to blend, continue
|
|
|
+ if (data.IsDeleted)
|
|
|
+ {
|
|
|
+ chunk.Dispose();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // nothing to blend with, swap
|
|
|
+ var maybeCommitted = MaybeGetCommittedChunk(pos, resolution);
|
|
|
+ if (maybeCommitted is null)
|
|
|
+ {
|
|
|
+ committedChunks[resolution].Add(pos, chunk);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ //blend
|
|
|
+ blendModePaint.BlendMode = blendMode;
|
|
|
+ if (lockTransparency)
|
|
|
+ {
|
|
|
+ using Chunk tempChunk = Chunk.Create(resolution);
|
|
|
+ tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(maybeCommitted.Surface.SkiaSurface, 0, 0, ReplacingPaint);
|
|
|
+ maybeCommitted.Surface.SkiaSurface.Canvas.DrawSurface(chunk.Surface.SkiaSurface, 0, 0, blendModePaint);
|
|
|
+ OperationHelper.ClampAlpha(maybeCommitted.Surface.SkiaSurface, tempChunk.Surface.SkiaSurface);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ maybeCommitted.Surface.SkiaSurface.Canvas.DrawSurface(chunk.Surface.SkiaSurface, 0, 0, blendModePaint);
|
|
|
+ }
|
|
|
+
|
|
|
+ chunk.Dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // delete committed low res chunks that weren't updated
|
|
|
+ foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
|
|
|
+ {
|
|
|
+ foreach (var (resolution, _) in latestChunks)
|
|
|
+ {
|
|
|
+ if (resolution == ChunkResolution.Full)
|
|
|
+ continue;
|
|
|
+ if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) || halfChunk.QueueProgress != queuedOperations.Count)
|
|
|
+ {
|
|
|
+ if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
|
|
|
+ {
|
|
|
+ committedChunks[resolution].Remove(pos);
|
|
|
+ committedLowResChunk.Dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // clear latest chunks
|
|
|
+ foreach (var (resolution, chunks) in latestChunks)
|
|
|
+ {
|
|
|
+ chunks.Clear();
|
|
|
+ latestChunksData[resolution].Clear();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <returns>
|
|
|
+ /// All chunks that have something in them, including latest (uncommitted) ones
|
|
|
+ /// </returns>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public HashSet<VecI> FindAllChunks()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
|
|
|
+ foreach (var (_, opChunks) in queuedOperations)
|
|
|
+ {
|
|
|
+ allChunks.UnionWith(opChunks);
|
|
|
+ }
|
|
|
+
|
|
|
+ return allChunks;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public HashSet<VecI> FindCommittedChunks()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <returns>
|
|
|
+ /// Chunks affected by operations that haven't been committed yet
|
|
|
+ /// </returns>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public HashSet<VecI> FindAffectedChunks(int fromOperationIndex = 0)
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ var chunks = new HashSet<VecI>();
|
|
|
+ for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
|
|
|
+ {
|
|
|
+ var (_, opChunks) = queuedOperations[i];
|
|
|
+ chunks.UnionWith(opChunks);
|
|
|
+ }
|
|
|
+
|
|
|
+ return chunks;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Applies all operations queued for a specific (latest) chunk. If the latest chunk doesn't exist yet, creates it. If none of the existing operations affect the chunk does nothing.
|
|
|
+ /// </summary>
|
|
|
+ private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
|
|
|
+ {
|
|
|
+ if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
|
|
|
+ chunkData = new() { QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos) };
|
|
|
+ if (chunkData.QueueProgress == queuedOperations.Count)
|
|
|
+ return;
|
|
|
+
|
|
|
+ Chunk? targetChunk = null;
|
|
|
+ OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips = new FilledChunk();
|
|
|
+
|
|
|
+ bool initialized = false;
|
|
|
+
|
|
|
+ for (int i = 0; i < queuedOperations.Count; i++)
|
|
|
+ {
|
|
|
+ var (operation, operChunks) = queuedOperations[i];
|
|
|
+ if (!operChunks.Contains(chunkPos))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ if (!initialized)
|
|
|
+ {
|
|
|
+ initialized = true;
|
|
|
+ targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
|
|
|
+ combinedRasterClips = CombineRasterClipsForChunk(chunkPos, resolution);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (chunkData.QueueProgress <= i)
|
|
|
+ chunkData.IsDeleted = ApplyOperationToChunk(operation, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (initialized)
|
|
|
+ {
|
|
|
+ if (lockTransparency && !chunkData.IsDeleted && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
|
|
|
+ {
|
|
|
+ var committed = GetCommittedChunk(chunkPos, resolution);
|
|
|
+ OperationHelper.ClampAlpha(targetChunk!.Surface.SkiaSurface, committed!.Surface.SkiaSurface);
|
|
|
+ }
|
|
|
+
|
|
|
+ chunkData.QueueProgress = queuedOperations.Count;
|
|
|
+ latestChunksData[resolution][chunkPos] = chunkData;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (combinedRasterClips.TryPickT2(out Chunk value, out var _))
|
|
|
+ value.Dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ private OneOf<FilledChunk, EmptyChunk, Chunk> CombineRasterClipsForChunk(VecI chunkPos, ChunkResolution resolution)
|
|
|
+ {
|
|
|
+ if (lockTransparency && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is null)
|
|
|
+ {
|
|
|
+ return new EmptyChunk();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (activeClips.Count == 0)
|
|
|
+ {
|
|
|
+ return new FilledChunk();
|
|
|
+ }
|
|
|
+
|
|
|
+ var intersection = Chunk.Create(resolution);
|
|
|
+ intersection.Surface.SkiaSurface.Canvas.Clear(SKColors.White);
|
|
|
+
|
|
|
+ foreach (var mask in activeClips)
|
|
|
+ {
|
|
|
+ if (mask.CommittedChunkExists(chunkPos))
|
|
|
+ {
|
|
|
+ mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.SkiaSurface, VecI.Zero, ClippingPaint);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ intersection.Dispose();
|
|
|
+ return new EmptyChunk();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return intersection;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <returns>
|
|
|
+ /// True if the chunk was fully cleared (and should be deleted).
|
|
|
+ /// </returns>
|
|
|
+ private bool ApplyOperationToChunk(
|
|
|
+ IOperation operation,
|
|
|
+ OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
|
|
|
+ Chunk targetChunk,
|
|
|
+ VecI chunkPos,
|
|
|
+ ChunkResolution resolution,
|
|
|
+ LatestChunkData chunkData)
|
|
|
+ {
|
|
|
+ if (operation is ClearOperation)
|
|
|
+ return true;
|
|
|
+
|
|
|
+ if (operation is IDrawOperation chunkOperation)
|
|
|
+ {
|
|
|
+ if (combinedRasterClips.IsT1) //Nothing is visible
|
|
|
+ return chunkData.IsDeleted;
|
|
|
+
|
|
|
+ if (chunkData.IsDeleted)
|
|
|
+ targetChunk.Surface.SkiaSurface.Canvas.Clear();
|
|
|
+
|
|
|
+ // just regular drawing
|
|
|
+ if (combinedRasterClips.IsT0) //Everything is visible as far as raster clips are concerned
|
|
|
+ {
|
|
|
+ CallDrawWithClip(chunkOperation, targetChunk, resolution, chunkPos);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // drawing with raster clipping
|
|
|
+ var clip = combinedRasterClips.AsT2;
|
|
|
+
|
|
|
+ using var tempChunk = Chunk.Create(targetChunk.Resolution);
|
|
|
+ targetChunk.DrawOnSurface(tempChunk.Surface.SkiaSurface, VecI.Zero, ReplacingPaint);
|
|
|
+
|
|
|
+ CallDrawWithClip(chunkOperation, tempChunk, resolution, chunkPos);
|
|
|
+
|
|
|
+ clip.DrawOnSurface(tempChunk.Surface.SkiaSurface, VecI.Zero, ClippingPaint);
|
|
|
+ clip.DrawOnSurface(targetChunk.Surface.SkiaSurface, VecI.Zero, InverseClippingPaint);
|
|
|
+
|
|
|
+ tempChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, VecI.Zero, AddingPaint);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (operation is ResizeOperation resizeOperation)
|
|
|
+ {
|
|
|
+ return IsOutsideBounds(chunkPos, resizeOperation.Size);
|
|
|
+ }
|
|
|
+
|
|
|
+ return chunkData.IsDeleted;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void CallDrawWithClip(IDrawOperation operation, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
|
|
|
+ {
|
|
|
+ if (clippingPath is not null && !clippingPath.IsEmpty)
|
|
|
+ {
|
|
|
+ int count = targetChunk.Surface.SkiaSurface.Canvas.Save();
|
|
|
+
|
|
|
+ using SKPath transformedPath = new(clippingPath);
|
|
|
+ float scale = (float)resolution.Multiplier();
|
|
|
+ VecD trans = -chunkPos * FullChunkSize * scale;
|
|
|
+ transformedPath.Transform(SKMatrix.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
|
|
|
+ targetChunk.Surface.SkiaSurface.Canvas.ClipPath(transformedPath);
|
|
|
+ operation.DrawOnChunk(targetChunk, chunkPos);
|
|
|
+ targetChunk.Surface.SkiaSurface.Canvas.RestoreToCount(count);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ operation.DrawOnChunk(targetChunk, chunkPos);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Finds and deletes empty committed chunks. Returns true if all existing chunks were deleted.
|
|
|
+ /// Note: this function modifies the internal state, it is not thread safe! Use it only in changes (same as all the other functions that change the image in some way).
|
|
|
+ /// </summary>
|
|
|
+ /// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
|
+ public bool CheckIfCommittedIsEmpty()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ ThrowIfDisposed();
|
|
|
+ if (queuedOperations.Count > 0)
|
|
|
+ throw new InvalidOperationException("This function can only be used when there are no queued operations");
|
|
|
+ FindAndDeleteEmptyCommittedChunks();
|
|
|
+ return committedChunks[ChunkResolution.Full].Count == 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private HashSet<VecI> FindAllChunksOutsideBounds(VecI size)
|
|
|
+ {
|
|
|
+ var chunks = FindAllChunks();
|
|
|
+ chunks.RemoveWhere(pos => !IsOutsideBounds(pos, size));
|
|
|
+ return chunks;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
|
|
|
+ {
|
|
|
+ return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X || chunkPos.Y * FullChunkSize >= imageSize.Y;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void FindAndDeleteEmptyCommittedChunks()
|
|
|
+ {
|
|
|
+ if (queuedOperations.Count != 0)
|
|
|
+ throw new InvalidOperationException("This method cannot be used while any operations are queued");
|
|
|
+ HashSet<VecI> toRemove = new();
|
|
|
+ foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
|
|
|
+ {
|
|
|
+ if (chunk.Surface.IsFullyTransparent())
|
|
|
+ {
|
|
|
+ toRemove.Add(pos);
|
|
|
+ chunk.Dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var pos in toRemove)
|
|
|
+ {
|
|
|
+ committedChunks[ChunkResolution.Full].Remove(pos);
|
|
|
+ committedChunks[ChunkResolution.Half].Remove(pos);
|
|
|
+ committedChunks[ChunkResolution.Quarter].Remove(pos);
|
|
|
+ committedChunks[ChunkResolution.Eighth].Remove(pos);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets existing committed chunk or creates a new one. Doesn't apply any operations to the chunk, returns it as it is.
|
|
|
+ /// </summary>
|
|
|
+ private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
|
|
|
+ {
|
|
|
+ // committed chunk of the same resolution exists
|
|
|
+ Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
|
|
|
+ if (targetChunk is not null)
|
|
|
+ return targetChunk;
|
|
|
+
|
|
|
+ // for full res chunks: nothing exists, create brand new chunk
|
|
|
+ if (resolution == ChunkResolution.Full)
|
|
|
+ {
|
|
|
+ var newChunk = Chunk.Create(resolution);
|
|
|
+ committedChunks[resolution][chunkPos] = newChunk;
|
|
|
+ return newChunk;
|
|
|
+ }
|
|
|
+
|
|
|
+ // for low res chunks: full res version exists
|
|
|
+ Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ if (existingFullResChunk is not null)
|
|
|
+ {
|
|
|
+ var newChunk = Chunk.Create(resolution);
|
|
|
+ newChunk.Surface.SkiaSurface.Canvas.Save();
|
|
|
+ newChunk.Surface.SkiaSurface.Canvas.Scale((float)resolution.Multiplier());
|
|
|
+
|
|
|
+ newChunk.Surface.SkiaSurface.Canvas.DrawSurface(existingFullResChunk.Surface.SkiaSurface, 0, 0, SmoothReplacingPaint);
|
|
|
+ newChunk.Surface.SkiaSurface.Canvas.Restore();
|
|
|
+ committedChunks[resolution][chunkPos] = newChunk;
|
|
|
+ return newChunk;
|
|
|
+ }
|
|
|
+
|
|
|
+ // for low res chunks: full res version doesn't exist
|
|
|
+ {
|
|
|
+ GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ var newChunk = Chunk.Create(resolution);
|
|
|
+ committedChunks[resolution][chunkPos] = newChunk;
|
|
|
+ return newChunk;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets existing latest chunk or creates a new one, based on a committed one if it exists. Doesn't do any operations to the chunk.
|
|
|
+ /// </summary>
|
|
|
+ private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
|
|
|
+ {
|
|
|
+ // latest chunk exists
|
|
|
+ Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
|
|
|
+ if (targetChunk is not null)
|
|
|
+ return targetChunk;
|
|
|
+
|
|
|
+ // committed chunk of the same resolution exists
|
|
|
+ var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
|
|
|
+ if (maybeCommittedAnyRes is not null)
|
|
|
+ {
|
|
|
+ Chunk newChunk = Chunk.Create(resolution);
|
|
|
+ if (blendMode == SKBlendMode.Src)
|
|
|
+ maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
|
|
|
+ else
|
|
|
+ newChunk.Surface.SkiaSurface.Canvas.Clear();
|
|
|
+ latestChunks[resolution][chunkPos] = newChunk;
|
|
|
+ return newChunk;
|
|
|
+ }
|
|
|
+
|
|
|
+ // committed chunk of full resolution exists
|
|
|
+ var maybeCommittedFullRes = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
|
+ if (maybeCommittedFullRes is not null)
|
|
|
+ {
|
|
|
+ //create low res committed chunk
|
|
|
+ var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
|
|
|
+ //create latest based on it
|
|
|
+ Chunk newChunk = Chunk.Create(resolution);
|
|
|
+ committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
|
|
|
+ latestChunks[resolution][chunkPos] = newChunk;
|
|
|
+ return newChunk;
|
|
|
+ }
|
|
|
+
|
|
|
+ // no previous chunks exist
|
|
|
+ var newLatestChunk = Chunk.Create(resolution);
|
|
|
+ newLatestChunk.Surface.SkiaSurface.Canvas.Clear();
|
|
|
+ latestChunks[resolution][chunkPos] = newLatestChunk;
|
|
|
+ return newLatestChunk;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ThrowIfDisposed()
|
|
|
+ {
|
|
|
+ if (disposed)
|
|
|
+ throw new ObjectDisposedException(nameof(ChunkyImage));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void Dispose()
|
|
|
+ {
|
|
|
+ lock (lockObject)
|
|
|
+ {
|
|
|
+ if (disposed)
|
|
|
+ return;
|
|
|
+ CancelChanges();
|
|
|
+ DisposeAll();
|
|
|
+ blendModePaint.Dispose();
|
|
|
+ GC.SuppressFinalize(this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void DisposeAll()
|
|
|
+ {
|
|
|
+ foreach (var (_, chunks) in committedChunks)
|
|
|
+ {
|
|
|
+ foreach (var (_, chunk) in chunks)
|
|
|
+ {
|
|
|
+ chunk.Dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var (_, chunks) in latestChunks)
|
|
|
+ {
|
|
|
+ foreach (var (_, chunk) in chunks)
|
|
|
+ {
|
|
|
+ chunk.Dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ disposed = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ ~ChunkyImage()
|
|
|
+ {
|
|
|
+ DisposeAll();
|
|
|
+ }
|
|
|
+}
|