|
@@ -4,11 +4,15 @@ using ChunkyImageLib.DataHolders;
|
|
using ChunkyImageLib.Operations;
|
|
using ChunkyImageLib.Operations;
|
|
using OneOf;
|
|
using OneOf;
|
|
using OneOf.Types;
|
|
using OneOf.Types;
|
|
|
|
+using PixiEditor.Common;
|
|
|
|
+using PixiEditor.DrawingApi.Core;
|
|
using PixiEditor.DrawingApi.Core.ColorsImpl;
|
|
using PixiEditor.DrawingApi.Core.ColorsImpl;
|
|
using PixiEditor.DrawingApi.Core.Numerics;
|
|
using PixiEditor.DrawingApi.Core.Numerics;
|
|
-using PixiEditor.DrawingApi.Core.Surface;
|
|
|
|
-using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
|
|
|
|
-using PixiEditor.DrawingApi.Core.Surface.Vector;
|
|
|
|
|
|
+using PixiEditor.DrawingApi.Core.Surfaces;
|
|
|
|
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
|
|
|
|
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
|
|
|
|
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
|
|
|
|
+using PixiEditor.Numerics;
|
|
|
|
|
|
[assembly: InternalsVisibleTo("ChunkyImageLibTest")]
|
|
[assembly: InternalsVisibleTo("ChunkyImageLibTest")]
|
|
|
|
|
|
@@ -39,7 +43,7 @@ namespace ChunkyImageLib;
|
|
/// - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
|
|
/// - 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.
|
|
/// </summary>
|
|
/// </summary>
|
|
-public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
|
|
|
|
+public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICacheable
|
|
{
|
|
{
|
|
private struct LatestChunkData
|
|
private struct LatestChunkData
|
|
{
|
|
{
|
|
@@ -61,10 +65,15 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
|
|
private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
|
|
private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
|
|
private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
|
|
private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
|
|
private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
|
|
- private static Paint SmoothReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
|
|
|
|
|
|
+
|
|
|
|
+ private static Paint SmoothReplacingPaint { get; } =
|
|
|
|
+ new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
|
|
|
|
+
|
|
private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
|
|
private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
|
|
private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
|
|
private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
|
|
|
|
|
|
|
|
+ public int CommitCounter => commitCounter;
|
|
|
|
+
|
|
public VecI CommittedSize { get; private set; }
|
|
public VecI CommittedSize { get; private set; }
|
|
public VecI LatestSize { get; private set; }
|
|
public VecI LatestSize { get; private set; }
|
|
|
|
|
|
@@ -84,6 +93,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
private VectorPath? clippingPath;
|
|
private VectorPath? clippingPath;
|
|
private double? horizontalSymmetryAxis = null;
|
|
private double? horizontalSymmetryAxis = null;
|
|
private double? verticalSymmetryAxis = null;
|
|
private double? verticalSymmetryAxis = null;
|
|
|
|
+
|
|
|
|
+ private int operationCounter = 0;
|
|
|
|
|
|
private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
|
|
private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
|
|
private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
|
|
private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
|
|
@@ -116,6 +127,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ public ChunkyImage(Surface image) : this(image.Size)
|
|
|
|
+ {
|
|
|
|
+ EnqueueDrawImage(VecI.Zero, image);
|
|
|
|
+ CommitChanges();
|
|
|
|
+ }
|
|
|
|
+
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
public RectI? FindChunkAlignedMostUpToDateBounds()
|
|
public RectI? FindChunkAlignedMostUpToDateBounds()
|
|
{
|
|
{
|
|
@@ -129,6 +146,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
rect ??= chunkBounds;
|
|
rect ??= chunkBounds;
|
|
rect = rect.Value.Union(chunkBounds);
|
|
rect = rect.Value.Union(chunkBounds);
|
|
}
|
|
}
|
|
|
|
+
|
|
foreach (var operation in queuedOperations)
|
|
foreach (var operation in queuedOperations)
|
|
{
|
|
{
|
|
foreach (var pos in operation.affectedArea.Chunks)
|
|
foreach (var pos in operation.affectedArea.Chunks)
|
|
@@ -138,6 +156,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
rect = rect.Value.Union(chunkBounds);
|
|
rect = rect.Value.Union(chunkBounds);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
return rect;
|
|
return rect;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -155,6 +174,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
rect ??= chunkBounds;
|
|
rect ??= chunkBounds;
|
|
rect = rect.Value.Union(chunkBounds);
|
|
rect = rect.Value.Union(chunkBounds);
|
|
}
|
|
}
|
|
|
|
+
|
|
return rect;
|
|
return rect;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -178,7 +198,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
if (committedChunks[suggestedResolution].TryGetValue(chunkPos, out Chunk? requestedResChunk))
|
|
if (committedChunks[suggestedResolution].TryGetValue(chunkPos, out Chunk? requestedResChunk))
|
|
{
|
|
{
|
|
- RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize)).Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
|
|
|
|
|
|
+ RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
|
|
|
|
+ .Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
|
|
|
|
|
|
RectI? chunkPreciseBounds = requestedResChunk.FindPreciseBounds(visibleArea);
|
|
RectI? chunkPreciseBounds = requestedResChunk.FindPreciseBounds(visibleArea);
|
|
if (chunkPreciseBounds is null)
|
|
if (chunkPreciseBounds is null)
|
|
@@ -190,17 +211,20 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
else
|
|
else
|
|
{
|
|
{
|
|
- RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize)).Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
|
|
|
|
|
|
+ RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
|
|
|
|
+ .Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
|
|
|
|
|
|
RectI? chunkPreciseBounds = fullResChunk.FindPreciseBounds(visibleArea);
|
|
RectI? chunkPreciseBounds = fullResChunk.FindPreciseBounds(visibleArea);
|
|
if (chunkPreciseBounds is null)
|
|
if (chunkPreciseBounds is null)
|
|
continue;
|
|
continue;
|
|
- RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier).Offset(chunkPos * chunkSize).RoundOutwards();
|
|
|
|
|
|
+ RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
|
|
|
|
+ .Offset(chunkPos * chunkSize).RoundOutwards();
|
|
|
|
|
|
preciseBounds ??= globalChunkBounds;
|
|
preciseBounds ??= globalChunkBounds;
|
|
preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
|
|
preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
|
|
preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
|
|
preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
|
|
preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
|
|
|
|
|
|
@@ -280,12 +304,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
|
|
Chunk? latestChunk = GetLatestChunk(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);
|
|
|
|
|
|
+ Color committedColor = committedChunk is null
|
|
|
|
+ ? Colors.Transparent
|
|
|
|
+ : committedChunk.Surface.GetSRGBPixel(posInChunk);
|
|
|
|
+ Color latestColor = latestChunk is null
|
|
|
|
+ ? Colors.Transparent
|
|
|
|
+ : latestChunk.Surface.GetSRGBPixel(posInChunk);
|
|
// using a whole chunk just to draw 1 pixel is kinda dumb,
|
|
// using a whole chunk just to draw 1 pixel is kinda dumb,
|
|
// but this should be faster than any approach that requires allocations
|
|
// but this should be faster than any approach that requires allocations
|
|
using Chunk tempChunk = Chunk.Create(ChunkResolution.Eighth);
|
|
using Chunk tempChunk = Chunk.Create(ChunkResolution.Eighth);
|
|
@@ -302,7 +326,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
/// True if the chunk existed and was drawn, otherwise false
|
|
/// True if the chunk existed and was drawn, otherwise false
|
|
/// </returns>
|
|
/// </returns>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
- public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null)
|
|
|
|
|
|
+ public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos,
|
|
|
|
+ Paint? paint = null)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
{
|
|
{
|
|
@@ -321,13 +346,13 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
|
|
|
|
var committedChunk = GetCommittedChunk(chunkPos, resolution);
|
|
var committedChunk = GetCommittedChunk(chunkPos, resolution);
|
|
-
|
|
|
|
|
|
+
|
|
// draw committed directly
|
|
// draw committed directly
|
|
if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
|
|
if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
|
|
{
|
|
{
|
|
if (committedChunk is null)
|
|
if (committedChunk is null)
|
|
return false;
|
|
return false;
|
|
- committedChunk.DrawOnSurface(surface, pos, paint);
|
|
|
|
|
|
+ committedChunk.DrawChunkOn(surface, pos, paint);
|
|
return true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -336,7 +361,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
if (latestChunk.IsT2)
|
|
if (latestChunk.IsT2)
|
|
{
|
|
{
|
|
- latestChunk.AsT2.DrawOnSurface(surface, pos, paint);
|
|
|
|
|
|
+ latestChunk.AsT2.DrawChunkOn(surface, pos, paint);
|
|
return true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -345,12 +370,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
|
|
|
|
// combine with committed and then draw
|
|
// combine with committed and then draw
|
|
using var tempChunk = Chunk.Create(resolution);
|
|
using var tempChunk = Chunk.Create(resolution);
|
|
- tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0, ReplacingPaint);
|
|
|
|
|
|
+ tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
|
|
|
|
+ ReplacingPaint);
|
|
blendModePaint.BlendMode = blendMode;
|
|
blendModePaint.BlendMode = blendMode;
|
|
- tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0, blendModePaint);
|
|
|
|
|
|
+ tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
|
|
|
|
+ blendModePaint);
|
|
if (lockTransparency)
|
|
if (lockTransparency)
|
|
OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
|
|
OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
|
|
- tempChunk.DrawOnSurface(surface, pos, paint);
|
|
|
|
|
|
+ tempChunk.DrawChunkOn(surface, pos, paint);
|
|
|
|
|
|
return true;
|
|
return true;
|
|
}
|
|
}
|
|
@@ -375,8 +402,25 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ public bool LatestOrCommittedChunkExists()
|
|
|
|
+ {
|
|
|
|
+ lock (lockObject)
|
|
|
|
+ {
|
|
|
|
+ ThrowIfDisposed();
|
|
|
|
+ var chunks = FindAllChunks();
|
|
|
|
+ foreach (var chunk in chunks)
|
|
|
|
+ {
|
|
|
|
+ if (LatestOrCommittedChunkExists(chunk))
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
- public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null)
|
|
|
|
|
|
+ public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos,
|
|
|
|
+ Paint? paint = null)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
{
|
|
{
|
|
@@ -384,7 +428,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
var chunk = GetCommittedChunk(chunkPos, resolution);
|
|
var chunk = GetCommittedChunk(chunkPos, resolution);
|
|
if (chunk is null)
|
|
if (chunk is null)
|
|
return false;
|
|
return false;
|
|
- chunk.DrawOnSurface(surface, pos, paint);
|
|
|
|
|
|
+ chunk.DrawChunkOn(surface, pos, paint);
|
|
return true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -441,7 +485,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
if (queuedOperations.Count > 0)
|
|
if (queuedOperations.Count > 0)
|
|
- throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "This function can only be executed when there are no queued operations");
|
|
activeClips.Add(clippingMask);
|
|
activeClips.Add(clippingMask);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -453,7 +498,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
if (queuedOperations.Count > 0)
|
|
if (queuedOperations.Count > 0)
|
|
- throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "This function can only be executed when there are no queued operations");
|
|
this.clippingPath = clippingPath;
|
|
this.clippingPath = clippingPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -468,7 +514,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
if (queuedOperations.Count > 0)
|
|
if (queuedOperations.Count > 0)
|
|
- throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "This function can only be executed when there are no queued operations");
|
|
blendMode = mode;
|
|
blendMode = mode;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -480,7 +527,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
if (queuedOperations.Count > 0)
|
|
if (queuedOperations.Count > 0)
|
|
- throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "This function can only be executed when there are no queued operations");
|
|
horizontalSymmetryAxis = position;
|
|
horizontalSymmetryAxis = position;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -492,7 +540,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
if (queuedOperations.Count > 0)
|
|
if (queuedOperations.Count > 0)
|
|
- throw new InvalidOperationException("This function can only be executed when there are no queued operations");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "This function can only be executed when there are no queued operations");
|
|
verticalSymmetryAxis = position;
|
|
verticalSymmetryAxis = position;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -530,7 +579,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
|
|
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
- public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth, Paint? paint = null)
|
|
|
|
|
|
+ public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth,
|
|
|
|
+ Paint? paint = null)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
{
|
|
{
|
|
@@ -584,7 +634,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
EnqueueOperation(operation);
|
|
EnqueueOperation(operation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
|
|
+
|
|
public void EnqueueApplyMask(ChunkyImage mask)
|
|
public void EnqueueApplyMask(ChunkyImage mask)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
@@ -597,7 +647,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
|
|
|
|
/// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
|
|
/// <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>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
- public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap, BlendMode blendMode, RectI? customBounds = null)
|
|
|
|
|
|
+ public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap,
|
|
|
|
+ BlendMode blendMode, RectI? customBounds = null)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
{
|
|
{
|
|
@@ -619,7 +670,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
|
|
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
- public void EnqueueDrawSkiaLine(VecI from, VecI to, StrokeCap strokeCap, float strokeWidth, Color color, BlendMode blendMode)
|
|
|
|
|
|
+ public void EnqueueDrawSkiaLine(VecI from, VecI to, StrokeCap strokeCap, float strokeWidth, Color color,
|
|
|
|
+ BlendMode blendMode)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
{
|
|
{
|
|
@@ -663,16 +715,23 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
|
|
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
- public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
|
|
|
|
|
|
+ public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
|
|
{
|
|
{
|
|
lock (lockObject)
|
|
lock (lockObject)
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
- ChunkyImageOperation operation = new(image, pos, flipHor, flipVer);
|
|
|
|
|
|
+ ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, false);
|
|
EnqueueOperation(operation);
|
|
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);
|
|
|
|
+ }
|
|
|
|
+
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
/// <exception cref="ObjectDisposedException">This image is disposed</exception>
|
|
public void EnqueueClearRegion(RectI region)
|
|
public void EnqueueClearRegion(RectI region)
|
|
{
|
|
{
|
|
@@ -718,6 +777,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+
|
|
|
|
+ public void EnqueueDrawPaint(Paint paint)
|
|
|
|
+ {
|
|
|
|
+ lock (lockObject)
|
|
|
|
+ {
|
|
|
|
+ ThrowIfDisposed();
|
|
|
|
+ PaintOperation operation = new(paint);
|
|
|
|
+ EnqueueOperation(operation);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
private void EnqueueOperation(IDrawOperation operation)
|
|
private void EnqueueOperation(IDrawOperation operation)
|
|
{
|
|
{
|
|
List<IDrawOperation> operations = new(4) { operation };
|
|
List<IDrawOperation> operations = new(4) { operation };
|
|
@@ -740,6 +810,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
if (operation.IgnoreEmptyChunks)
|
|
if (operation.IgnoreEmptyChunks)
|
|
area.Chunks.IntersectWith(FindAllChunks());
|
|
area.Chunks.IntersectWith(FindAllChunks());
|
|
EnqueueOperation(op, area);
|
|
EnqueueOperation(op, area);
|
|
|
|
+ operationCounter++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -835,7 +906,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
if (resolution == ChunkResolution.Full)
|
|
if (resolution == ChunkResolution.Full)
|
|
{
|
|
{
|
|
- throw new InvalidOperationException("Trying to commit a full res chunk that wasn't fully processed");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "Trying to commit a full res chunk that wasn't fully processed");
|
|
}
|
|
}
|
|
else
|
|
else
|
|
{
|
|
{
|
|
@@ -884,13 +956,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
if (lockTransparency)
|
|
if (lockTransparency)
|
|
{
|
|
{
|
|
using Chunk tempChunk = Chunk.Create(resolution);
|
|
using Chunk tempChunk = Chunk.Create(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.DrawingSurface, tempChunk.Surface.DrawingSurface);
|
|
|
|
|
|
+ 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.DrawingSurface,
|
|
|
|
+ tempChunk.Surface.DrawingSurface);
|
|
}
|
|
}
|
|
else
|
|
else
|
|
{
|
|
{
|
|
- maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0, blendModePaint);
|
|
|
|
|
|
+ maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
|
|
|
|
+ blendModePaint);
|
|
}
|
|
}
|
|
|
|
|
|
chunk.Dispose();
|
|
chunk.Dispose();
|
|
@@ -905,7 +981,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
if (resolution == ChunkResolution.Full)
|
|
if (resolution == ChunkResolution.Full)
|
|
continue;
|
|
continue;
|
|
- if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) || halfChunk.QueueProgress != queuedOperations.Count)
|
|
|
|
|
|
+ if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) ||
|
|
|
|
+ halfChunk.QueueProgress != queuedOperations.Count)
|
|
{
|
|
{
|
|
if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
|
|
if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
|
|
{
|
|
{
|
|
@@ -964,7 +1041,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
var chunks = new HashSet<VecI>();
|
|
var chunks = new HashSet<VecI>();
|
|
RectI? rect = null;
|
|
RectI? rect = null;
|
|
-
|
|
|
|
|
|
+
|
|
for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
|
|
for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
|
|
{
|
|
{
|
|
var (_, area) = queuedOperations[i];
|
|
var (_, area) = queuedOperations[i];
|
|
@@ -979,13 +1056,25 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ public void SetCommitedChunk(Chunk chunk, VecI pos, ChunkResolution resolution)
|
|
|
|
+ {
|
|
|
|
+ lock (lockObject)
|
|
|
|
+ {
|
|
|
|
+ ThrowIfDisposed();
|
|
|
|
+ committedChunks[resolution][pos] = chunk;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
/// <summary>
|
|
/// <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.
|
|
/// 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>
|
|
/// </summary>
|
|
private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
|
|
private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
|
|
{
|
|
{
|
|
if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
|
|
if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
|
|
- chunkData = new() { QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos) };
|
|
|
|
|
|
+ chunkData = new()
|
|
|
|
+ {
|
|
|
|
+ QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos)
|
|
|
|
+ };
|
|
if (chunkData.QueueProgress == queuedOperations.Count)
|
|
if (chunkData.QueueProgress == queuedOperations.Count)
|
|
return;
|
|
return;
|
|
|
|
|
|
@@ -1008,12 +1097,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
}
|
|
}
|
|
|
|
|
|
if (chunkData.QueueProgress <= i)
|
|
if (chunkData.QueueProgress <= i)
|
|
- chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
|
|
|
|
|
|
+ chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!,
|
|
|
|
+ chunkPos, resolution, chunkData);
|
|
}
|
|
}
|
|
|
|
|
|
if (initialized)
|
|
if (initialized)
|
|
{
|
|
{
|
|
- if (lockTransparency && !chunkData.IsDeleted && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
|
|
|
|
|
|
+ if (lockTransparency && !chunkData.IsDeleted &&
|
|
|
|
+ MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
|
|
{
|
|
{
|
|
var committed = GetCommittedChunk(chunkPos, resolution);
|
|
var committed = GetCommittedChunk(chunkPos, resolution);
|
|
OperationHelper.ClampAlpha(targetChunk!.Surface.DrawingSurface, committed!.Surface.DrawingSurface);
|
|
OperationHelper.ClampAlpha(targetChunk!.Surface.DrawingSurface, committed!.Surface.DrawingSurface);
|
|
@@ -1046,7 +1137,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
if (mask.CommittedChunkExists(chunkPos))
|
|
if (mask.CommittedChunkExists(chunkPos))
|
|
{
|
|
{
|
|
- mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
|
|
|
|
|
|
+ mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero,
|
|
|
|
+ ClippingPaint);
|
|
}
|
|
}
|
|
else
|
|
else
|
|
{
|
|
{
|
|
@@ -1092,14 +1184,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
var clip = combinedRasterClips.AsT2;
|
|
var clip = combinedRasterClips.AsT2;
|
|
|
|
|
|
using var tempChunk = Chunk.Create(targetChunk.Resolution);
|
|
using var tempChunk = Chunk.Create(targetChunk.Resolution);
|
|
- targetChunk.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
|
|
|
|
|
|
+ targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
|
|
|
|
|
|
CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
|
|
CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
|
|
|
|
|
|
- clip.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
|
|
|
|
- clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
|
|
|
|
|
|
+ clip.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
|
|
|
|
+ clip.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
|
|
|
|
|
|
- tempChunk.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
|
|
|
|
|
|
+ tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
|
|
return false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -1111,7 +1203,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
return chunkData.IsDeleted;
|
|
return chunkData.IsDeleted;
|
|
}
|
|
}
|
|
|
|
|
|
- private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
|
|
|
|
|
|
+ private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk,
|
|
|
|
+ ChunkResolution resolution, VecI chunkPos)
|
|
{
|
|
{
|
|
if (operationAffectedArea is null)
|
|
if (operationAffectedArea is null)
|
|
return;
|
|
return;
|
|
@@ -1123,7 +1216,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
using VectorPath transformedPath = new(clippingPath);
|
|
using VectorPath transformedPath = new(clippingPath);
|
|
VecD trans = -chunkPos * FullChunkSize * scale;
|
|
VecD trans = -chunkPos * FullChunkSize * scale;
|
|
-
|
|
|
|
|
|
+
|
|
transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
|
|
transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
|
|
targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
|
|
targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
|
|
}
|
|
}
|
|
@@ -1149,7 +1242,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
{
|
|
{
|
|
ThrowIfDisposed();
|
|
ThrowIfDisposed();
|
|
if (queuedOperations.Count > 0)
|
|
if (queuedOperations.Count > 0)
|
|
- throw new InvalidOperationException("This function can only be used when there are no queued operations");
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "This function can only be used when there are no queued operations");
|
|
FindAndDeleteEmptyCommittedChunks();
|
|
FindAndDeleteEmptyCommittedChunks();
|
|
return committedChunks[ChunkResolution.Full].Count == 0;
|
|
return committedChunks[ChunkResolution.Full].Count == 0;
|
|
}
|
|
}
|
|
@@ -1164,7 +1258,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
|
|
|
|
private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
|
|
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;
|
|
|
|
|
|
+ return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X ||
|
|
|
|
+ chunkPos.Y * FullChunkSize >= imageSize.Y;
|
|
}
|
|
}
|
|
|
|
|
|
private void FindAndDeleteEmptyCommittedChunks()
|
|
private void FindAndDeleteEmptyCommittedChunks()
|
|
@@ -1216,7 +1311,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
newChunk.Surface.DrawingSurface.Canvas.Save();
|
|
newChunk.Surface.DrawingSurface.Canvas.Save();
|
|
newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
|
|
newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
|
|
|
|
|
|
- newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0, SmoothReplacingPaint);
|
|
|
|
|
|
+ newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0,
|
|
|
|
+ SmoothReplacingPaint);
|
|
newChunk.Surface.DrawingSurface.Canvas.Restore();
|
|
newChunk.Surface.DrawingSurface.Canvas.Restore();
|
|
committedChunks[resolution][chunkPos] = newChunk;
|
|
committedChunks[resolution][chunkPos] = newChunk;
|
|
return newChunk;
|
|
return newChunk;
|
|
@@ -1313,4 +1409,23 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
|
|
|
|
|
|
disposed = true;
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ public object Clone()
|
|
|
|
+ {
|
|
|
|
+ lock (lockObject)
|
|
|
|
+ {
|
|
|
|
+ ThrowIfDisposed();
|
|
|
|
+ ChunkyImage clone = CloneFromCommitted();
|
|
|
|
+ return clone;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public int GetCacheHash()
|
|
|
|
+ {
|
|
|
|
+ return commitCounter + queuedOperations.Count + operationCounter + activeClips.Count
|
|
|
|
+ + (int)blendMode + (lockTransparency ? 1 : 0)
|
|
|
|
+ + (horizontalSymmetryAxis is not null ? (int)(horizontalSymmetryAxis * 100) : 0)
|
|
|
|
+ + (verticalSymmetryAxis is not null ? (int)(verticalSymmetryAxis * 100) : 0)
|
|
|
|
+ + (clippingPath is not null ? 1 : 0);
|
|
|
|
+ }
|
|
}
|
|
}
|