using System.Runtime.CompilerServices;
using ChunkyImageLib.DataHolders;
using ChunkyImageLib.Operations;
using OneOf;
using OneOf.Types;
using PixiEditor.Common;
using Drawie.Backend.Core;
using Drawie.Backend.Core.Bridge;
using Drawie.Backend.Core.ColorsImpl;
using Drawie.Backend.Core.ColorsImpl.Paintables;
using Drawie.Backend.Core.Numerics;
using Drawie.Backend.Core.Shaders;
using Drawie.Backend.Core.Surfaces;
using Drawie.Backend.Core.Surfaces.ImageData;
using Drawie.Backend.Core.Surfaces.PaintImpl;
using Drawie.Backend.Core.Vector;
using Drawie.Numerics;
[assembly: InternalsVisibleTo("ChunkyImageLibTest")]
namespace ChunkyImageLib;
public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICacheable
{
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;
private int cancelCounter = 0;
private RectI cachedPreciseCommitedBounds = RectI.Empty;
private RectI cachedPreciseLatestBounds = RectI.Empty;
private int lastCommitedBoundsCacheHash = -1;
private int lastLatestBoundsCacheHash = -1;
public const int FullChunkSize = ChunkPool.FullChunkSize;
private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
private static Paint SmoothReplacingPaint { get; } =
new Paint() { BlendMode = BlendMode.Src };
private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
public ColorSpace ProcessingColorSpace { get; }
public int CommitCounter => commitCounter;
public int CancelCounter => cancelCounter;
public VecI CommittedSize { get; private set; }
public VecI LatestSize { get; private set; }
public int QueueLength
{
get
{
lock (lockObject)
return queuedOperations.Count;
}
}
public double? HorizontalSymmetry => horizontalSymmetryAxis;
public double? VerticalSymmetry => verticalSymmetryAxis;
private readonly List<(IOperation operation, AffectedArea affectedArea)> queuedOperations = new();
private readonly List<ChunkyImage> activeClips = new();
private BlendMode blendMode = BlendMode.Src;
private bool lockTransparency = false;
private VectorPath? clippingPath;
private double? horizontalSymmetryAxis = null;
private double? verticalSymmetryAxis = null;
private int operationCounter = 0;
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 = CreateChunkResolutionDictionary<Chunk>();
latestChunks = CreateChunkResolutionDictionary<Chunk>();
latestChunksData = CreateChunkResolutionDictionary<LatestChunkData>();
ProcessingColorSpace = ColorSpace.CreateSrgb();
}
private static Dictionary<ChunkResolution, Dictionary<VecI, T>> CreateChunkResolutionDictionary<T>() =>
new()
{
[ChunkResolution.Full] = new Dictionary<VecI, T>(),
[ChunkResolution.Half] = new Dictionary<VecI, T>(),
[ChunkResolution.Quarter] = new Dictionary<VecI, T>(),
[ChunkResolution.Eighth] = new Dictionary<VecI, T>(),
};
public ChunkyImage(Surface image) : this(image.Size)
{
EnqueueDrawImage(VecI.Zero, image);
CommitChanges();
}
public RectI? FindChunkAlignedMostUpToDateBounds()
{
lock (lockObject)
{
ThrowIfDisposed();
RectI? rect = null;
foreach (var pos in committedChunks[ChunkResolution.Full].Keys)
{
RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
rect ??= chunkBounds;
rect = rect.Value.Union(chunkBounds);
}
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;
}
}
public RectI? FindChunkAlignedCommittedBounds()
{
lock (lockObject)
{
ThrowIfDisposed();
RectI? rect = null;
foreach (var pos in committedChunks[ChunkResolution.Full].Keys)
{
RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
rect ??= chunkBounds;
rect = rect.Value.Union(chunkBounds);
}
return rect;
}
}
public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
bool fallbackToChunkAligned = false)
{
lock (lockObject)
{
ThrowIfDisposed();
if (lastCommitedBoundsCacheHash == GetCacheHash())
{
return cachedPreciseCommitedBounds;
}
var chunkSize = suggestedResolution.PixelSize();
var multiplier = suggestedResolution.Multiplier();
RectI scaledCommittedSize = (RectI)(new RectD(VecI.Zero, CommittedSize * multiplier)).RoundOutwards();
RectI? preciseBounds = null;
foreach (var (chunkPos, fullResChunk) in committedChunks[ChunkResolution.Full])
{
if (committedChunks[suggestedResolution].TryGetValue(chunkPos, out Chunk? requestedResChunk))
{
RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
.Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
RectI? chunkPreciseBounds = requestedResChunk.FindPreciseBounds(visibleArea);
if (chunkPreciseBounds is null)
continue;
RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
preciseBounds ??= globalChunkBounds;
preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
}
else
{
if (fallbackToChunkAligned)
{
return FindChunkAlignedCommittedBounds();
}
RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
.Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
RectI? chunkPreciseBounds = fullResChunk.FindPreciseBounds(visibleArea);
if (chunkPreciseBounds is null)
continue;
RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
.Offset(chunkPos * chunkSize).RoundOutwards();
preciseBounds ??= globalChunkBounds;
preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
}
}
preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
cachedPreciseCommitedBounds = preciseBounds.GetValueOrDefault();
lastCommitedBoundsCacheHash = GetCacheHash();
return preciseBounds;
}
}
public RectI? FindTightLatestBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
bool fallbackToChunkAligned = false)
{
lock (lockObject)
{
ThrowIfDisposed();
if (queuedOperations.Count == 0)
{
return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
}
if (lastLatestBoundsCacheHash == GetCacheHash())
{
return cachedPreciseLatestBounds;
}
var chunkSize = suggestedResolution.PixelSize();
var multiplier = suggestedResolution.Multiplier();
RectI scaledLatestSize = (RectI)(new RectD(VecI.Zero, LatestSize * multiplier)).RoundOutwards();
RectI? preciseBounds = null;
var possibleChunks = new HashSet<VecI>();
foreach (var pos in committedChunks[ChunkResolution.Full].Keys)
possibleChunks.Add(pos);
foreach (var pos in latestChunks[ChunkResolution.Full].Keys)
possibleChunks.Add(pos);
foreach (var chunkPos in possibleChunks)
{
var committedChunk = GetCommittedChunk(chunkPos, suggestedResolution);
var latestChunk = GetLatestChunk(chunkPos, suggestedResolution);
Chunk? chunk;
bool isTempChunk = false;
if (latestChunk != null && committedChunk != null)
{
var tempChunk = Chunk.Create(ProcessingColorSpace, suggestedResolution);
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
ReplacingPaint);
blendModePaint.BlendMode = blendMode;
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.Surface.DrawingSurface, 0, 0,
blendModePaint);
if (lockTransparency)
OperationHelper.ClampAlpha(tempChunk.Surface,
committedChunk.Surface);
chunk = tempChunk;
isTempChunk = true;
}
else if (latestChunk != null)
{
chunk = latestChunk;
}
else
{
chunk = committedChunk;
}
if (chunk != null)
{
RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
.Intersect(scaledLatestSize).Translate(-chunkPos * chunkSize);
RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
if (chunkPreciseBounds is null)
continue;
RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
preciseBounds ??= globalChunkBounds;
preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
if (isTempChunk)
{
chunk.Dispose();
}
}
else
{
if (fallbackToChunkAligned)
{
return FindChunkAlignedMostUpToDateBounds();
}
RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
.Intersect(new RectI(VecI.Zero, LatestSize)).Translate(-chunkPos * FullChunkSize);
RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
if (chunkPreciseBounds is null)
continue;
RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
.Offset(chunkPos * chunkSize).RoundOutwards();
preciseBounds ??= globalChunkBounds;
preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
}
}
preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, LatestSize));
cachedPreciseLatestBounds = preciseBounds.GetValueOrDefault();
lastLatestBoundsCacheHash = GetCacheHash();
return preciseBounds;
}
}
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.EnqueueDrawTexture(chunk * FullChunkSize, image.Surface);
}
output.CommitChanges();
return output;
}
}
public ChunkyImage CloneFromLatest()
{
lock (lockObject)
{
ThrowIfDisposed();
ChunkyImage output = new(LatestSize);
var chunks = FindAllChunks();
foreach (var chunk in chunks)
{
var image = GetLatestChunk(chunk, ChunkResolution.Full);
if (image is null)
{
image = GetCommittedChunk(chunk, ChunkResolution.Full);
if (image is null)
continue;
}
output.EnqueueDrawTexture(chunk * FullChunkSize, image.Surface);
}
output.CommitChanges();
return output;
}
}
public Color GetCommittedPixel(VecI posOnImage)
{
lock (lockObject)
{
ThrowIfDisposed();
var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
var posInChunk = posOnImage - chunkPos * FullChunkSize;
return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
{
null => Colors.Transparent,
var chunk => chunk.Surface.GetSrgbPixel(posInChunk)
};
}
}
public Color GetCommittedPixelRaw(VecI posOnImage)
{
lock (lockObject)
{
ThrowIfDisposed();
var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
var posInChunk = posOnImage - chunkPos * FullChunkSize;
return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
{
null => Colors.Transparent,
var chunk => chunk.Surface.GetRawPixel(posInChunk)
};
}
}
public Color GetMostUpToDatePixel(VecI posOnImage)
{
lock (lockObject)
{
ThrowIfDisposed();
var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
var posInChunk = posOnImage - chunkPos * FullChunkSize;
if (queuedOperations.Count == 0)
{
Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
return committedChunk switch
{
null => Colors.Transparent,
_ => committedChunk.Surface.GetSrgbPixel(posInChunk)
};
}
if (blendMode == BlendMode.Src)
{
Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
return latestChunk switch
{
null => Colors.Transparent,
_ => latestChunk.Surface.GetSrgbPixel(posInChunk)
};
}
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
Color committedColor = committedChunk is null
? Colors.Transparent
: committedChunk.Surface.GetSrgbPixel(posInChunk);
Color latestColor = latestChunk is null
? Colors.Transparent
: latestChunk.Surface.GetSrgbPixel(posInChunk);
using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecD(1)), committedPaint);
tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecI(1)), latestPaint);
return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
}
}
}
public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos,
Paint? paint = null, SamplingOptions? samplingOptions = 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);
if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && !BlendModeNeedsSource())
{
if (committedChunk is null)
return false;
committedChunk.DrawChunkOn(surface, pos, paint, samplingOptions);
return true;
}
if (blendMode == BlendMode.Src || committedChunk is null)
{
if (latestChunk.IsT2 && blendMode != BlendMode.DstOut)
{
latestChunk.AsT2.DrawChunkOn(surface, pos, paint, samplingOptions);
return true;
}
return false;
}
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
ReplacingPaint);
blendModePaint.BlendMode = blendMode;
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
blendModePaint);
if (lockTransparency)
OperationHelper.ClampAlpha(tempChunk.Surface, committedChunk.Surface);
tempChunk.DrawChunkOn(surface, pos, paint, samplingOptions);
return true;
}
}
private bool BlendModeNeedsSource()
{
return blendMode is BlendMode.DstIn or BlendMode.DstOut;
}
public bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface,
VecD pos,
Paint? paint = null, SamplingOptions? sampling = null)
{
lock (lockObject)
{
ThrowIfDisposed();
OneOf<None, EmptyChunk, Chunk> latestChunk;
{
var chunk = MaybeGetLatestChunk(chunkPos, resolution);
if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
{
latestChunk = new EmptyChunk();
}
else
{
if (chunk is null && queuedOperations.Count > 0 && resolution != ChunkResolution.Full &&
MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) != null)
{
chunk = GetLatestChunk(chunkPos, resolution);
}
latestChunk = chunk is null ? new None() : chunk;
}
}
var committedChunk = GetCommittedChunk(chunkPos, resolution);
if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && !BlendModeNeedsSource())
{
if (committedChunk is null)
return false;
committedChunk.DrawChunkOn(surface, pos, paint, sampling);
return true;
}
if (blendMode == BlendMode.Src || committedChunk is null)
{
if (latestChunk.IsT2)
{
var originalBlendMode = paint?.BlendMode ?? BlendMode.SrcOver;
if (paint != null && BlendModeNeedsSource())
{
paint.BlendMode = blendMode;
}
latestChunk.AsT2.DrawChunkOn(surface, pos, paint, sampling);
if (paint != null)
{
paint.BlendMode = originalBlendMode;
}
return true;
}
return false;
}
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
ReplacingPaint);
blendModePaint.BlendMode = blendMode;
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
blendModePaint);
if (lockTransparency)
OperationHelper.ClampAlpha(tempChunk.Surface, committedChunk.Surface);
tempChunk.DrawChunkOn(surface, pos, paint, sampling);
return true;
}
}
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.affectedArea.Chunks.Contains(chunkPos))
return true;
}
return false;
}
}
public bool LatestOrCommittedChunkExists()
{
lock (lockObject)
{
ThrowIfDisposed();
var chunks = FindAllChunks();
foreach (var chunk in chunks)
{
if (LatestOrCommittedChunkExists(chunk))
return true;
}
}
return false;
}
public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos,
Paint? paint = null, SamplingOptions? samplingOptions = null)
{
lock (lockObject)
{
ThrowIfDisposed();
var chunk = GetCommittedChunk(chunkPos, resolution);
if (chunk is null)
return false;
chunk.DrawChunkOn(surface, pos, paint, samplingOptions);
return true;
}
}
internal bool CommittedChunkExists(VecI chunkPos)
{
lock (lockObject)
{
ThrowIfDisposed();
return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
}
}
private Chunk? GetLatestChunk(VecI pos, ChunkResolution resolution)
{
if (queuedOperations.Count == 0)
return null;
MaybeCreateAndProcessQueueForChunk(pos, resolution);
var maybeNewlyProcessedChunk = MaybeGetLatestChunk(pos, resolution);
return maybeNewlyProcessedChunk;
}
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].GetValueOrDefault(pos);
private Chunk? MaybeGetCommittedChunk(VecI pos, ChunkResolution resolution)
=> committedChunks[resolution].GetValueOrDefault(pos);
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);
}
}
public void SetClippingPath(VectorPath 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;
}
}
public void SetBlendMode(BlendMode 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;
}
}
public void SetOpacity(double opacity)
{
lock (lockObject)
{
ThrowIfDisposed();
if (queuedOperations.Count > 0)
throw new InvalidOperationException(
"This function can only be executed when there are no queued operations");
blendModePaint.Color = blendModePaint.Color.WithAlpha((byte)(opacity * 255));
}
}
public void SetHorizontalAxisOfSymmetry(double 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;
}
}
public void SetVerticalAxisOfSymmetry(double 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;
}
}
public void EnableLockTransparency()
{
lock (lockObject)
{
ThrowIfDisposed();
lockTransparency = true;
}
}
public void EnqueueReplaceColor(Color oldColor, Color newColor)
{
lock (lockObject)
{
ThrowIfDisposed();
ReplaceColorOperation operation = new(oldColor, newColor);
EnqueueOperation(operation);
}
}
public void EnqueueDrawRectangle(ShapeData rect)
{
lock (lockObject)
{
ThrowIfDisposed();
RectangleOperation operation = new(rect);
EnqueueOperation(operation);
}
}
public void EnqueueDrawEllipse(RectD location, Paintable? strokeColor, Paintable? fillColor, float strokeWidth,
double rotationRad = 0, bool antiAliased = false,
Paint? paint = null)
{
lock (lockObject)
{
ThrowIfDisposed();
EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased,
paint);
EnqueueOperation(operation);
}
}
public void EnqueueDrawImage(Matrix3X3 transformMatrix, Surface image, SamplingOptions samplingOptions,
Paint? paint = null, bool copyImage = true)
{
lock (lockObject)
{
ThrowIfDisposed();
ImageOperation operation = new(transformMatrix, image, samplingOptions, paint, copyImage);
EnqueueOperation(operation);
}
}
public void EnqueueDrawImage(ShapeCorners corners, Surface image, Paint? paint = null, bool copyImage = true)
{
lock (lockObject)
{
ThrowIfDisposed();
ImageOperation operation = new(corners, image, paint, copyImage);
EnqueueOperation(operation);
}
}
public void EnqueueDrawImage(VecI pos, Surface image, Paint? paint = null, bool copyImage = true)
{
lock (lockObject)
{
ThrowIfDisposed();
ImageOperation operation = new(pos, image, paint, copyImage);
EnqueueOperation(operation);
}
}
public void EnqueueDrawTexture(Matrix3X3 transformMatrix, Texture image, Paint? paint = null, bool copyImage = true)
{
lock (lockObject)
{
ThrowIfDisposed();
TextureOperation operation = new(transformMatrix, image, paint, copyImage);
EnqueueOperation(operation);
}
}
public void EnqueueDrawTexture(ShapeCorners corners, Texture image, Paint? paint = null, bool copyImage = true)
{
lock (lockObject)
{
ThrowIfDisposed();
TextureOperation operation = new(corners, image, paint, copyImage);
EnqueueOperation(operation);
}
}
public void EnqueueDrawTexture(VecI pos, Texture image, Paint? paint = null, bool copyImage = true)
{
lock (lockObject)
{
ThrowIfDisposed();
TextureOperation operation = new(pos, image, paint, copyImage);
EnqueueOperation(operation);
}
}
public void EnqueueApplyMask(ChunkyImage mask)
{
lock (lockObject)
{
ThrowIfDisposed();
ApplyMaskOperation operation = new(mask);
EnqueueOperation(operation);
}
}
public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap,
BlendMode blendMode, RectI? customBounds = null)
{
lock (lockObject)
{
ThrowIfDisposed();
PathOperation operation = new(path, color, strokeWidth, strokeCap, blendMode, customBounds);
EnqueueOperation(operation);
}
}
public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
BlendMode blendMode, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
{
lock (lockObject)
{
ThrowIfDisposed();
PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blendMode, style, antiAliasing,
customBounds);
EnqueueOperation(operation);
}
}
public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
{
lock (lockObject)
{
ThrowIfDisposed();
PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blender, style, antiAliasing,
customBounds);
EnqueueOperation(operation);
}
}
public void EnqueueNonMirroredDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds = null,
Matrix3X3? paintTransform = null)
{
lock (lockObject)
{
ThrowIfDisposed();
NonMirroredPathOperation operation = new(path, paintable, strokeWidth, strokeCap, blender, style,
antiAliasing,
customBounds, paintTransform);
EnqueueOperation(operation);
}
}
public void EnqueueNonMirroredDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
BlendMode blendMode, PaintStyle style, bool antiAliasing, RectI? customBounds = null,
Matrix3X3? paintTransform = null)
{
lock (lockObject)
{
ThrowIfDisposed();
NonMirroredPathOperation operation = new(path, paintable, strokeWidth, strokeCap, blendMode, style,
antiAliasing,
customBounds, paintTransform);
EnqueueOperation(operation);
}
}
public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
{
lock (lockObject)
{
ThrowIfDisposed();
BresenhamLineOperation operation = new(from, to, paintable, blendMode);
EnqueueOperation(operation);
}
}
public void EnqueueDrawSkiaLine(VecD from, VecD to, StrokeCap strokeCap, float strokeWidth, Color color,
BlendMode blendMode)
{
lock (lockObject)
{
ThrowIfDisposed();
DrawingSurfaceLineOperation operation = new(from, to, strokeCap, strokeWidth, color, blendMode);
EnqueueOperation(operation);
}
}
public void EnqueueDrawSkiaLine(VecD from, VecD to, Paint paint)
{
lock (lockObject)
{
ThrowIfDisposed();
DrawingSurfaceLineOperation operation = new(from, to, paint);
EnqueueOperation(operation);
}
}
public void EnqueueDrawPixels(IEnumerable<VecI> pixels, Color color, BlendMode blendMode)
{
lock (lockObject)
{
ThrowIfDisposed();
PixelsOperation operation = new(pixels, color, blendMode);
EnqueueOperation(operation);
}
}
public void EnqueueDrawPixel(VecI pos, Color color, BlendMode blendMode)
{
lock (lockObject)
{
ThrowIfDisposed();
PixelOperation operation = new(pos, color, blendMode);
EnqueueOperation(operation);
}
}
public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
{
lock (lockObject)
{
ThrowIfDisposed();
ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, false);
EnqueueOperation(operation);
}
}
public void EnqueueDrawUpToDateChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
{
ThrowIfDisposed();
ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, true);
EnqueueOperation(operation);
}
public void EnqueueClearRegion(RectI region)
{
lock (lockObject)
{
ThrowIfDisposed();
ClearRegionOperation operation = new(region);
EnqueueOperation(operation);
}
}
public void EnqueueClearPath(VectorPath path, RectI? pathTightBounds = null)
{
lock (lockObject)
{
ThrowIfDisposed();
ClearPathOperation operation = new(path, pathTightBounds);
EnqueueOperation(operation);
}
}
public void EnqueueClear()
{
lock (lockObject)
{
ThrowIfDisposed();
ClearOperation operation = new();
EnqueueOperation(operation, new(FindAllChunks()));
}
}
public void EnqueueResize(VecI newSize)
{
lock (lockObject)
{
ThrowIfDisposed();
ResizeOperation operation = new(newSize);
LatestSize = newSize;
EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
}
}
public void EnqueueDrawPaint(Paint paint)
{
lock (lockObject)
{
ThrowIfDisposed();
PaintOperation operation = new(paint);
EnqueueOperation(operation);
}
}
private void EnqueueOperation(IDrawOperation operation)
{
List<IDrawOperation> operations = new(4) { operation };
if (operation is IMirroredDrawOperation mirroredOperation)
{
if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
if (horizontalSymmetryAxis is not null)
operations.Add(mirroredOperation.AsMirrored(null, horizontalSymmetryAxis));
if (verticalSymmetryAxis is not null)
operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, null));
}
foreach (var op in operations)
{
var area = op.FindAffectedArea(LatestSize);
area.Chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
area.GlobalArea = area.GlobalArea?.Intersect(new RectI(VecI.Zero, LatestSize));
if (operation.IgnoreEmptyChunks)
area.Chunks.IntersectWith(FindAllChunks());
EnqueueOperation(op, area);
operationCounter++;
}
}
private void EnqueueOperation(IOperation operation, AffectedArea area)
{
queuedOperations.Add((operation, area));
}
public void CancelChanges()
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
lock (lockObject)
{
ThrowIfDisposed();
foreach (var operation in queuedOperations)
operation.operation.Dispose();
queuedOperations.Clear();
activeClips.Clear();
blendMode = BlendMode.Src;
lockTransparency = false;
horizontalSymmetryAxis = null;
verticalSymmetryAxis = null;
clippingPath = null;
foreach (var chunksOfRes in latestChunks.Values)
{
foreach (var chunk in chunksOfRes.Values)
{
chunk.Dispose();
}
}
LatestSize = CommittedSize;
foreach (var (res, chunks) in latestChunks)
{
chunks.Clear();
latestChunksData[res].Clear();
}
cancelCounter++;
}
}
public void DiscardChanges()
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
lock (lockObject)
{
ThrowIfDisposed();
foreach (var operation in queuedOperations)
operation.operation.Dispose();
queuedOperations.Clear();
activeClips.Clear();
foreach (var chunksOfRes in latestChunks.Values)
{
foreach (var chunk in chunksOfRes.Values)
{
chunk.Dispose();
}
}
LatestSize = CommittedSize;
foreach (var (res, chunks) in latestChunks)
{
chunks.Clear();
latestChunksData[res].Clear();
}
cancelCounter++;
}
}
public void CommitChanges()
{
lock (lockObject)
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
ThrowIfDisposed();
var affectedArea = FindAffectedArea();
foreach (var chunk in affectedArea.Chunks)
{
MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
}
foreach (var (operation, _) in queuedOperations)
{
operation.Dispose();
}
CommitLatestChunks();
CommittedSize = LatestSize;
queuedOperations.Clear();
activeClips.Clear();
blendMode = BlendMode.Src;
lockTransparency = false;
horizontalSymmetryAxis = null;
verticalSymmetryAxis = null;
clippingPath = null;
commitCounter++;
if (commitCounter % 30 == 0)
FindAndDeleteEmptyCommittedChunks();
}
}
private void CommitLatestChunks()
{
foreach (var (resolution, chunks) in latestChunks)
{
foreach (var (pos, chunk) in chunks)
{
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;
}
}
if (blendMode == BlendMode.Src)
{
if (committedChunks[resolution].ContainsKey(pos))
{
var oldChunk = committedChunks[resolution][pos];
committedChunks[resolution].Remove(pos);
oldChunk.Dispose();
}
if (!data.IsDeleted)
committedChunks[resolution].Add(pos, chunk);
else
chunk.Dispose();
}
else
{
if (data.IsDeleted)
{
chunk.Dispose();
continue;
}
var maybeCommitted = MaybeGetCommittedChunk(pos, resolution);
if (maybeCommitted is null)
{
if (!BlendModeNeedsSource())
{
committedChunks[resolution].Add(pos, chunk);
}
continue;
}
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
blendModePaint.BlendMode = blendMode;
if (lockTransparency)
{
using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0,
ReplacingPaint);
maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
blendModePaint);
OperationHelper.ClampAlpha(maybeCommitted.Surface,
tempChunk.Surface);
}
else
{
maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
blendModePaint);
}
chunk.Dispose();
}
}
}
foreach (var pos in latestChunks[ChunkResolution.Full].Keys)
{
foreach (var resolution in latestChunks.Keys)
{
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();
}
}
}
}
foreach (var (resolution, chunks) in latestChunks)
{
chunks.Clear();
latestChunksData[resolution].Clear();
}
}
public HashSet<VecI> FindAllChunks()
{
lock (lockObject)
{
ThrowIfDisposed();
var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
foreach (var (_, affArea) in queuedOperations)
{
allChunks.UnionWith(affArea.Chunks);
}
return allChunks;
}
}
public HashSet<VecI> FindCommittedChunks()
{
lock (lockObject)
{
ThrowIfDisposed();
return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
}
}
public Dictionary<VecI, Surface> CloneAllCommitedNonEmptyChunks()
{
lock (lockObject)
{
ThrowIfDisposed();
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
var dict = new Dictionary<VecI, Surface>();
foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
{
if (chunk == null) continue;
if (chunk.FindPreciseBounds().HasValue)
{
if (chunk.Surface == null) continue;
var surf = new Surface(chunk.Surface.ImageInfo);
surf.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0);
dict[pos] = surf;
surf.DrawingSurface.Flush();
}
}
return dict;
}
}
public AffectedArea FindAffectedArea(int fromOperationIndex = 0)
{
lock (lockObject)
{
ThrowIfDisposed();
var chunks = new HashSet<VecI>();
RectI? rect = null;
for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
{
var (_, area) = queuedOperations[i];
chunks.UnionWith(area.Chunks);
rect ??= area.GlobalArea;
if (area.GlobalArea is not null && rect is not null)
rect = rect.Value.Union(area.GlobalArea.Value);
}
return new AffectedArea(chunks, rect);
}
}
public void SetCommitedChunk(Chunk chunk, VecI pos, ChunkResolution resolution)
{
lock (lockObject)
{
ThrowIfDisposed();
committedChunks[resolution][pos] = chunk;
}
}
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, affArea) = queuedOperations[i];
if (!affArea.Chunks.Contains(chunkPos))
continue;
if (!initialized)
{
initialized = true;
targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
combinedRasterClips = CombineRasterClipsForChunk(chunkPos, resolution);
}
if (chunkData.QueueProgress <= i)
chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, 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, committed!.Surface);
}
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(ProcessingColorSpace, resolution);
intersection.Surface.DrawingSurface.Canvas.Clear(Colors.White);
foreach (var mask in activeClips)
{
if (mask.CommittedChunkExists(chunkPos))
{
mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface.Canvas, VecI.Zero,
ClippingPaint);
}
else
{
intersection.Dispose();
return new EmptyChunk();
}
}
return intersection;
}
private bool ApplyOperationToChunk(
IOperation operation,
AffectedArea operationAffectedArea,
OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
Chunk targetChunk,
VecI chunkPos,
ChunkResolution resolution,
LatestChunkData chunkData)
{
if (operation is ClearOperation)
return true;
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
if (operation is IDrawOperation chunkOperation)
{
if (combinedRasterClips.IsT1)
return chunkData.IsDeleted;
if (chunkData.IsDeleted)
targetChunk.Surface.DrawingSurface.Canvas.Clear();
if (combinedRasterClips.IsT0)
{
CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, targetChunk, resolution, chunkPos);
return false;
}
var clip = combinedRasterClips.AsT2;
using var tempChunk = Chunk.Create(ProcessingColorSpace, targetChunk.Resolution);
targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface.Canvas, VecI.Zero, ReplacingPaint);
CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
clip.DrawChunkOn(tempChunk.Surface.DrawingSurface.Canvas, VecI.Zero, ClippingPaint);
clip.DrawChunkOn(targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, InverseClippingPaint);
tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, AddingPaint);
return false;
}
if (operation is ResizeOperation resizeOperation)
{
return IsOutsideBounds(chunkPos, resizeOperation.Size);
}
return chunkData.IsDeleted;
}
private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk,
ChunkResolution resolution, VecI chunkPos)
{
if (operationAffectedArea is null)
return;
var finalTarget = targetChunk;
int count = finalTarget.Surface.DrawingSurface.Canvas.Save();
float scale = (float)resolution.Multiplier();
if (clippingPath is not null && !clippingPath.IsDisposed && !clippingPath.IsEmpty)
{
using VectorPath transformedPath = new(clippingPath);
VecD trans = -chunkPos * FullChunkSize * scale;
transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
finalTarget.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
}
VecD affectedAreaPos = operationAffectedArea.Value.TopLeft;
VecD affectedAreaSize = operationAffectedArea.Value.Size;
affectedAreaPos = (affectedAreaPos - chunkPos * FullChunkSize) * scale;
affectedAreaSize = affectedAreaSize * scale;
finalTarget.Surface.DrawingSurface.Canvas.ClipRect(new RectD(affectedAreaPos, affectedAreaSize));
operation.DrawOnChunk(finalTarget, chunkPos);
targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
}
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;
}
}
#if DEBUG
public void SaveCommitedToDesktop()
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
var surface = Surface.ForDisplay(new VecI(LatestSize.X, LatestSize.Y));
this.DrawCommittedRegionOn(new RectD(VecI.Zero, LatestSize), ChunkResolution.Full,
surface.DrawingSurface.Canvas, VecI.Zero, ReplacingPaint);
surface.SaveToDesktop();
}
#endif
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);
}
}
private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
if (targetChunk is not null)
return targetChunk;
if (resolution == ChunkResolution.Full)
{
var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
committedChunks[resolution][chunkPos] = newChunk;
return newChunk;
}
Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
if (existingFullResChunk is not null)
{
var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
newChunk.Surface.DrawingSurface.Canvas.Save();
newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
using var snapshot = existingFullResChunk.Surface.DrawingSurface.Snapshot();
newChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, SamplingOptions.Bilinear,
SmoothReplacingPaint);
newChunk.Surface.DrawingSurface.Canvas.Restore();
committedChunks[resolution][chunkPos] = newChunk;
return newChunk;
}
{
GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
committedChunks[resolution][chunkPos] = newChunk;
return newChunk;
}
}
private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
{
using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
if (targetChunk is not null)
return targetChunk;
var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
if (maybeCommittedAnyRes is not null)
{
Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
if (blendMode == BlendMode.Src)
maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
else
newChunk.Surface.DrawingSurface.Canvas.Clear();
latestChunks[resolution][chunkPos] = newChunk;
return newChunk;
}
var maybeCommittedFullRes = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
if (maybeCommittedFullRes is not null)
{
var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
latestChunks[resolution][chunkPos] = newChunk;
return newChunk;
}
var newLatestChunk = Chunk.Create(ProcessingColorSpace, resolution);
newLatestChunk.Surface.DrawingSurface.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.Values)
{
foreach (var chunk in chunks.Values)
{
chunk.Dispose();
}
}
foreach (var chunks in latestChunks.Values)
{
foreach (var chunk in chunks.Values)
{
chunk.Dispose();
}
}
disposed = true;
}
public object Clone()
{
lock (lockObject)
{
ThrowIfDisposed();
ChunkyImage clone = CloneFromLatest();
return clone;
}
}
public int GetCacheHash()
{
HashCode hash = new HashCode();
hash.Add(commitCounter);
hash.Add(cancelCounter);
hash.Add(queuedOperations.Count);
hash.Add(operationCounter);
foreach (var queuedOperation in queuedOperations)
{
hash.Add(queuedOperation.affectedArea.GlobalArea?.GetHashCode() ?? 0);
hash.Add(queuedOperation.operation.GetHashCode());
}
hash.Add(activeClips.Count);
hash.Add((int)blendMode);
hash.Add(lockTransparency);
if (horizontalSymmetryAxis is not null)
hash.Add((int)(horizontalSymmetryAxis * 100));
if (verticalSymmetryAxis is not null)
hash.Add((int)(verticalSymmetryAxis * 100));
if (clippingPath is not null)
hash.Add(1);
return hash.ToHashCode();
}
}