12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271 |
- using System.ComponentModel.DataAnnotations;
- using System.Runtime.CompilerServices;
- using ChunkyImageLib.DataHolders;
- using ChunkyImageLib.Operations;
- using OneOf;
- using OneOf.Types;
- using PixiEditor.DrawingApi.Core.ColorsImpl;
- using PixiEditor.DrawingApi.Core.Numerics;
- using PixiEditor.DrawingApi.Core.Surface;
- using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
- using PixiEditor.DrawingApi.Core.Surface.Vector;
- [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:
- /// - BlendMode.Src: default mode, the latest chunks are the same as committed ones but with some or all queued operations applied.
- /// This means that operations can work with the existing pixels.
- /// - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
- /// They need to be drawn over the committed chunks to obtain the final image. In this case, operations won't have access to the existing pixels.
- /// </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 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, FilterQuality = FilterQuality.Medium };
- private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
- private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.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, 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 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 RectI? FindLatestBounds()
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- RectI? rect = null;
- foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
- {
- RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
- rect ??= chunkBounds;
- rect = rect.Value.Union(chunkBounds);
- }
- foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
- {
- RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
- rect ??= chunkBounds;
- rect = rect.Value.Union(chunkBounds);
- }
- return rect;
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public RectI? FindPreciseCommittedBounds()
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- RectI? preciseBounds = null;
- foreach (var (chunkPos, chunk) in committedChunks[ChunkResolution.Full])
- {
- RectI? chunkPreciseBounds = chunk.FindPreciseBounds();
- if(chunkPreciseBounds is null)
- continue;
- RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * FullChunkSize);
- preciseBounds ??= globalChunkBounds;
- preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
- }
- preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
- return preciseBounds;
- }
- }
- /// <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 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)
- };
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public Color 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 => Colors.Transparent,
- _ => committedChunk.Surface.GetSRGBPixel(posInChunk)
- };
- }
- // something is queued, blend mode is Src so no merging needed
- if (blendMode == BlendMode.Src)
- {
- Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
- return latestChunk switch
- {
- null => Colors.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);
- 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,
- // but this should be faster than any approach that requires allocations
- using Chunk tempChunk = Chunk.Create(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.DrawPixel(VecI.Zero, committedPaint);
- tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(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, DrawingSurface surface, VecI pos, Paint? 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 != BlendMode.Src)
- {
- if (committedChunk is null)
- return false;
- committedChunk.DrawOnSurface(surface, pos, paint);
- return true;
- }
- // no need to combine with committed, draw directly
- if (blendMode == BlendMode.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.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.DrawingSurface, committedChunk.Surface.DrawingSurface);
- 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.affectedArea.Chunks.Contains(chunkPos))
- return true;
- }
- return false;
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? 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(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;
- }
- }
- /// <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(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;
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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;
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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;
- }
- }
- /// <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 EnqueueReplaceColor(Color oldColor, Color newColor)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- ReplaceColorOperation operation = new(oldColor, newColor);
- EnqueueOperation(operation);
- }
- }
- /// <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, Color strokeColor, Color fillColor, int strokeWidth, Paint? 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(Matrix3X3 transformMatrix, Surface image, Paint? paint = null, bool copyImage = true)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- ImageOperation operation = new(transformMatrix, image, paint, copyImage);
- EnqueueOperation(operation);
- }
- }
- /// <summary>
- /// Be careful about the copyImage argument, see other overload for details
- /// </summary>
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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);
- }
- }
- /// <summary>
- /// Be careful about the copyImage argument, see other overload for details
- /// </summary>
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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 EnqueueApplyMask(ChunkyImage mask)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- ApplyMaskOperation operation = new(mask);
- 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(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);
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public void EnqueueDrawBresenhamLine(VecI from, VecI to, Color color, BlendMode 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, StrokeCap strokeCap, float strokeWidth, Color color, BlendMode blendMode)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- DrawingSurfaceLineOperation operation = new(from, to, strokeCap, strokeWidth, color, blendMode);
- EnqueueOperation(operation);
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public void EnqueueDrawPixels(IEnumerable<VecI> pixels, Color color, BlendMode 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, Color color, BlendMode blendMode)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- PixelOperation operation = new(pos, color, blendMode);
- EnqueueOperation(operation);
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- PixelOperation operation = new(pos, pixelProcessor, 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 EnqueueClearPath(VectorPath path, RectI? pathTightBounds = null)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- ClearPathOperation operation = new(path, pathTightBounds);
- EnqueueOperation(operation);
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public void EnqueueClear()
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- ClearOperation operation = new();
- EnqueueOperation(operation, new(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, new(FindAllChunksOutsideBounds(newSize)));
- }
- }
- 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);
- }
- }
- private void EnqueueOperation(IOperation operation, AffectedArea area)
- {
- queuedOperations.Add((operation, area));
- }
- /// <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 = BlendMode.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 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();
- }
- }
- /// <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 == BlendMode.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.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
- {
- maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 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 (_, affArea) in queuedOperations)
- {
- allChunks.UnionWith(affArea.Chunks);
- }
- 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 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);
- }
- }
- /// <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, 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.DrawingSurface, committed!.Surface.DrawingSurface);
- }
- 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.DrawingSurface.Canvas.Clear(Colors.White);
- foreach (var mask in activeClips)
- {
- if (mask.CommittedChunkExists(chunkPos))
- {
- mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, 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,
- AffectedArea operationAffectedArea,
- 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.DrawingSurface.Canvas.Clear();
- // just regular drawing
- if (combinedRasterClips.IsT0) // Everything is visible as far as the raster clips are concerned
- {
- CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, targetChunk, resolution, chunkPos);
- return false;
- }
- // drawing with raster clipping
- var clip = combinedRasterClips.AsT2;
- using var tempChunk = Chunk.Create(targetChunk.Resolution);
- targetChunk.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
- CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
- clip.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
- clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
- tempChunk.DrawOnSurface(targetChunk.Surface.DrawingSurface, 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;
- int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
- float scale = (float)resolution.Multiplier();
- if (clippingPath is not null && !clippingPath.IsEmpty)
- {
- using VectorPath transformedPath = new(clippingPath);
- VecD trans = -chunkPos * FullChunkSize * scale;
-
- transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
- targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
- }
- VecD affectedAreaPos = operationAffectedArea.Value.TopLeft;
- VecD affectedAreaSize = operationAffectedArea.Value.Size;
- affectedAreaPos = (affectedAreaPos - chunkPos * FullChunkSize) * scale;
- affectedAreaSize = affectedAreaSize * scale;
- targetChunk.Surface.DrawingSurface.Canvas.ClipRect(new RectD(affectedAreaPos, affectedAreaSize));
- operation.DrawOnChunk(targetChunk, chunkPos);
- targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
- }
- /// <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.DrawingSurface.Canvas.Save();
- newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
- newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0, SmoothReplacingPaint);
- newChunk.Surface.DrawingSurface.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 == BlendMode.Src)
- maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
- else
- newChunk.Surface.DrawingSurface.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.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)
- {
- foreach (var (_, chunk) in chunks)
- {
- chunk.Dispose();
- }
- }
- foreach (var (_, chunks) in latestChunks)
- {
- foreach (var (_, chunk) in chunks)
- {
- chunk.Dispose();
- }
- }
- disposed = true;
- }
- }
|