123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488 |
- using System.ComponentModel.DataAnnotations;
- using System.Runtime.CompilerServices;
- using ChunkyImageLib.DataHolders;
- using ChunkyImageLib.Operations;
- using OneOf;
- using OneOf.Types;
- using PixiEditor.Common;
- using PixiEditor.DrawingApi.Core;
- using PixiEditor.DrawingApi.Core.ColorsImpl;
- using PixiEditor.DrawingApi.Core.Numerics;
- 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")]
- 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, 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 RectI cachedPreciseBounds = RectI.Empty;
- private int lastBoundsCacheHash = -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, FilterQuality = FilterQuality.Medium };
- private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
- private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
- public int CommitCounter => commitCounter;
- 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 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 = 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(),
- };
- }
- public ChunkyImage(Surface image) : this(image.Size)
- {
- EnqueueDrawImage(VecI.Zero, image);
- CommitChanges();
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public RectI? FindChunkAlignedMostUpToDateBounds()
- {
- 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 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;
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public RectI? FindChunkAlignedCommittedBounds()
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- RectI? rect = null;
- foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
- {
- RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
- rect ??= chunkBounds;
- rect = rect.Value.Union(chunkBounds);
- }
- return rect;
- }
- }
- /// <summary>
- /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
- /// </summary>
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- if (lastBoundsCacheHash == GetCacheHash())
- {
- return cachedPreciseBounds;
- }
- 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
- {
- 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));
- cachedPreciseBounds = preciseBounds.GetValueOrDefault();
- lastBoundsCacheHash = GetCacheHash();
- 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.DrawChunkOn(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.DrawChunkOn(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.DrawChunkOn(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;
- }
- }
- 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>
- 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.DrawChunkOn(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.
- 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,
- double rotationRad = 0,
- Paint? paint = null)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, 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);
- }
- }
- /// <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.
- /// Texture is NOT THREAD SAFE, so if you pass a Texture here with copyImage == false you must not do anything with that texture anywhere (not even read) until CommitChanges/CancelChanges is called.
- /// </summary>
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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);
- }
- }
- /// <summary>
- /// Be careful about the copyImage argument, see other overload for details
- /// </summary>
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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);
- }
- }
- /// <summary>
- /// Be careful about the copyImage argument, see other overload for details
- /// </summary>
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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);
- }
- }
- /// <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, GetCommittedPixel, blendMode);
- EnqueueOperation(operation);
- }
- }
- /// <exception cref="ObjectDisposedException">This image is disposed</exception>
- 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);
- }
- /// <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)));
- }
- }
- 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));
- }
- /// <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);
- }
- }
- public void SetCommitedChunk(Chunk chunk, VecI pos, ChunkResolution resolution)
- {
- lock (lockObject)
- {
- ThrowIfDisposed();
- committedChunks[resolution][pos] = chunk;
- }
- }
- /// <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.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
- CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
- clip.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
- clip.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
- tempChunk.DrawChunkOn(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;
- }
- 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);
- }
- }
|