ChunkyImage.cs 67 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768
  1. using System.ComponentModel.DataAnnotations;
  2. using System.Diagnostics;
  3. using System.Runtime.CompilerServices;
  4. using ChunkyImageLib.DataHolders;
  5. using ChunkyImageLib.Operations;
  6. using OneOf;
  7. using OneOf.Types;
  8. using PixiEditor.Common;
  9. using Drawie.Backend.Core;
  10. using Drawie.Backend.Core.Bridge;
  11. using Drawie.Backend.Core.ColorsImpl;
  12. using Drawie.Backend.Core.ColorsImpl.Paintables;
  13. using Drawie.Backend.Core.Numerics;
  14. using Drawie.Backend.Core.Shaders;
  15. using Drawie.Backend.Core.Surfaces;
  16. using Drawie.Backend.Core.Surfaces.ImageData;
  17. using Drawie.Backend.Core.Surfaces.PaintImpl;
  18. using Drawie.Backend.Core.Vector;
  19. using Drawie.Numerics;
  20. [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
  21. namespace ChunkyImageLib;
  22. /// <summary>
  23. /// This class is thread-safe only for reading! Only the functions from IReadOnlyChunkyImage can be called from any thread.
  24. /// ChunkyImage can be in two general states:
  25. /// 1. a state with all chunks committed and no queued operations
  26. /// - latestChunks and latestChunksData are empty
  27. /// - queuedOperations are empty
  28. /// - committedChunks[ChunkResolution.Full] contains the current versions of all stored chunks
  29. /// - committedChunks[*any other resolution*] may contain the current low res versions of some of the chunks (or all of them, or none)
  30. /// - LatestSize == CommittedSize == current image size (px)
  31. /// 2. and a state with some queued operations
  32. /// - queuedOperations contains all requested operations (drawing, raster clips, clear, etc.)
  33. /// - committedChunks[ChunkResolution.Full] contains the last versions before any operations of all stored chunks
  34. /// - 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)
  35. /// - latestChunks stores chunks with some (or none, or all) queued operations applied
  36. /// - latestChunksData stores the data for some or all of the latest chunks (not necessarily synced with latestChunks).
  37. /// 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)
  38. /// - LatestSize contains the new size if any resize operations were requested, otherwise the committed size
  39. /// You can check the current state via queuedOperations.Count == 0
  40. ///
  41. /// Depending on the chosen blend mode the latest chunks contain different things:
  42. /// - BlendMode.Src: default mode, the latest chunks are the same as committed ones but with some or all queued operations applied.
  43. /// This means that operations can work with the existing pixels.
  44. /// - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
  45. /// 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.
  46. /// </summary>
  47. public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICacheable
  48. {
  49. private struct LatestChunkData
  50. {
  51. public LatestChunkData()
  52. {
  53. QueueProgress = 0;
  54. IsDeleted = false;
  55. }
  56. public int QueueProgress { get; set; }
  57. public bool IsDeleted { get; set; }
  58. }
  59. private bool disposed = false;
  60. private readonly object lockObject = new();
  61. private int commitCounter = 0;
  62. private RectI cachedPreciseCommitedBounds = RectI.Empty;
  63. private RectI cachedPreciseLatestBounds = RectI.Empty;
  64. private int lastCommitedBoundsCacheHash = -1;
  65. private int lastLatestBoundsCacheHash = -1;
  66. public const int FullChunkSize = ChunkPool.FullChunkSize;
  67. private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
  68. private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
  69. private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
  70. private static Paint SmoothReplacingPaint { get; } =
  71. new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
  72. private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
  73. private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
  74. public ColorSpace ProcessingColorSpace { get; set; }
  75. public int CommitCounter => commitCounter;
  76. public VecI CommittedSize { get; private set; }
  77. public VecI LatestSize { get; private set; }
  78. public int QueueLength
  79. {
  80. get
  81. {
  82. lock (lockObject)
  83. return queuedOperations.Count;
  84. }
  85. }
  86. private readonly List<(IOperation operation, AffectedArea affectedArea)> queuedOperations = new();
  87. private readonly List<ChunkyImage> activeClips = new();
  88. private BlendMode blendMode = BlendMode.Src;
  89. private bool lockTransparency = false;
  90. private VectorPath? clippingPath;
  91. private double? horizontalSymmetryAxis = null;
  92. private double? verticalSymmetryAxis = null;
  93. private int operationCounter = 0;
  94. private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
  95. private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
  96. private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
  97. public ChunkyImage(VecI size, ColorSpace colorSpace)
  98. {
  99. CommittedSize = size;
  100. LatestSize = size;
  101. committedChunks = new()
  102. {
  103. [ChunkResolution.Full] = new(),
  104. [ChunkResolution.Half] = new(),
  105. [ChunkResolution.Quarter] = new(),
  106. [ChunkResolution.Eighth] = new(),
  107. };
  108. latestChunks = new()
  109. {
  110. [ChunkResolution.Full] = new(),
  111. [ChunkResolution.Half] = new(),
  112. [ChunkResolution.Quarter] = new(),
  113. [ChunkResolution.Eighth] = new(),
  114. };
  115. latestChunksData = new()
  116. {
  117. [ChunkResolution.Full] = new(),
  118. [ChunkResolution.Half] = new(),
  119. [ChunkResolution.Quarter] = new(),
  120. [ChunkResolution.Eighth] = new(),
  121. };
  122. ProcessingColorSpace = colorSpace;
  123. }
  124. public ChunkyImage(Surface image, ColorSpace colorSpace) : this(image.Size, colorSpace)
  125. {
  126. EnqueueDrawImage(VecI.Zero, image);
  127. CommitChanges();
  128. }
  129. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  130. public RectI? FindChunkAlignedMostUpToDateBounds()
  131. {
  132. lock (lockObject)
  133. {
  134. ThrowIfDisposed();
  135. RectI? rect = null;
  136. foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
  137. {
  138. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  139. rect ??= chunkBounds;
  140. rect = rect.Value.Union(chunkBounds);
  141. }
  142. foreach (var operation in queuedOperations)
  143. {
  144. foreach (var pos in operation.affectedArea.Chunks)
  145. {
  146. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  147. rect ??= chunkBounds;
  148. rect = rect.Value.Union(chunkBounds);
  149. }
  150. }
  151. return rect;
  152. }
  153. }
  154. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  155. public RectI? FindChunkAlignedCommittedBounds()
  156. {
  157. lock (lockObject)
  158. {
  159. ThrowIfDisposed();
  160. RectI? rect = null;
  161. foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
  162. {
  163. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  164. rect ??= chunkBounds;
  165. rect = rect.Value.Union(chunkBounds);
  166. }
  167. return rect;
  168. }
  169. }
  170. /// <summary>
  171. /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
  172. /// </summary>
  173. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  174. public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
  175. bool fallbackToChunkAligned = false)
  176. {
  177. lock (lockObject)
  178. {
  179. ThrowIfDisposed();
  180. if (lastCommitedBoundsCacheHash == GetCacheHash())
  181. {
  182. return cachedPreciseCommitedBounds;
  183. }
  184. var chunkSize = suggestedResolution.PixelSize();
  185. var multiplier = suggestedResolution.Multiplier();
  186. RectI scaledCommittedSize = (RectI)(new RectD(VecI.Zero, CommittedSize * multiplier)).RoundOutwards();
  187. RectI? preciseBounds = null;
  188. foreach (var (chunkPos, fullResChunk) in committedChunks[ChunkResolution.Full])
  189. {
  190. if (committedChunks[suggestedResolution].TryGetValue(chunkPos, out Chunk? requestedResChunk))
  191. {
  192. RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
  193. .Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
  194. RectI? chunkPreciseBounds = requestedResChunk.FindPreciseBounds(visibleArea);
  195. if (chunkPreciseBounds is null)
  196. continue;
  197. RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
  198. preciseBounds ??= globalChunkBounds;
  199. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  200. }
  201. else
  202. {
  203. if (fallbackToChunkAligned)
  204. {
  205. return FindChunkAlignedCommittedBounds();
  206. }
  207. RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
  208. .Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
  209. RectI? chunkPreciseBounds = fullResChunk.FindPreciseBounds(visibleArea);
  210. if (chunkPreciseBounds is null)
  211. continue;
  212. RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
  213. .Offset(chunkPos * chunkSize).RoundOutwards();
  214. preciseBounds ??= globalChunkBounds;
  215. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  216. }
  217. }
  218. preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
  219. preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
  220. cachedPreciseCommitedBounds = preciseBounds.GetValueOrDefault();
  221. lastCommitedBoundsCacheHash = GetCacheHash();
  222. return preciseBounds;
  223. }
  224. }
  225. public RectI? FindTightLatestBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
  226. bool fallbackToChunkAligned = false)
  227. {
  228. lock (lockObject)
  229. {
  230. ThrowIfDisposed();
  231. if(queuedOperations.Count == 0)
  232. {
  233. return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
  234. }
  235. if (lastLatestBoundsCacheHash == GetCacheHash())
  236. {
  237. return cachedPreciseLatestBounds;
  238. }
  239. var chunkSize = suggestedResolution.PixelSize();
  240. var multiplier = suggestedResolution.Multiplier();
  241. RectI scaledLatestSize = (RectI)(new RectD(VecI.Zero, LatestSize * multiplier)).RoundOutwards();
  242. RectI? preciseBounds = null;
  243. var possibleChunks = new HashSet<VecI>();
  244. foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
  245. possibleChunks.Add(pos);
  246. foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
  247. possibleChunks.Add(pos);
  248. foreach (var chunkPos in possibleChunks)
  249. {
  250. var committedChunk = GetCommittedChunk(chunkPos, suggestedResolution);
  251. var latestChunk = GetLatestChunk(chunkPos, suggestedResolution);
  252. Chunk? chunk;
  253. bool isTempChunk = false;
  254. if (latestChunk != null && committedChunk != null)
  255. {
  256. // both exist, need to merge
  257. var tempChunk = Chunk.Create(ProcessingColorSpace, suggestedResolution);
  258. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
  259. ReplacingPaint);
  260. blendModePaint.BlendMode = blendMode;
  261. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.Surface.DrawingSurface, 0, 0,
  262. blendModePaint);
  263. if (lockTransparency)
  264. OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface,
  265. committedChunk.Surface.DrawingSurface);
  266. chunk = tempChunk;
  267. isTempChunk = true;
  268. }
  269. else if (latestChunk != null)
  270. {
  271. chunk = latestChunk;
  272. }
  273. else
  274. {
  275. chunk = committedChunk;
  276. }
  277. if (chunk != null)
  278. {
  279. RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
  280. .Intersect(scaledLatestSize).Translate(-chunkPos * chunkSize);
  281. RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
  282. if (chunkPreciseBounds is null)
  283. continue;
  284. RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
  285. preciseBounds ??= globalChunkBounds;
  286. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  287. if (isTempChunk)
  288. {
  289. chunk.Dispose();
  290. }
  291. }
  292. else
  293. {
  294. if (fallbackToChunkAligned)
  295. {
  296. return FindChunkAlignedMostUpToDateBounds();
  297. }
  298. RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
  299. .Intersect(new RectI(VecI.Zero, LatestSize)).Translate(-chunkPos * FullChunkSize);
  300. RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
  301. if (chunkPreciseBounds is null)
  302. continue;
  303. RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
  304. .Offset(chunkPos * chunkSize).RoundOutwards();
  305. preciseBounds ??= globalChunkBounds;
  306. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  307. }
  308. }
  309. preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
  310. preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, LatestSize));
  311. cachedPreciseLatestBounds = preciseBounds.GetValueOrDefault();
  312. lastLatestBoundsCacheHash = GetCacheHash();
  313. return preciseBounds;
  314. }
  315. }
  316. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  317. public ChunkyImage CloneFromCommitted()
  318. {
  319. lock (lockObject)
  320. {
  321. ThrowIfDisposed();
  322. ChunkyImage output = new(LatestSize, ProcessingColorSpace);
  323. var chunks = FindCommittedChunks();
  324. foreach (var chunk in chunks)
  325. {
  326. var image = GetCommittedChunk(chunk, ChunkResolution.Full);
  327. if (image is null)
  328. continue;
  329. output.EnqueueDrawTexture(chunk * FullChunkSize, image.Surface);
  330. }
  331. output.CommitChanges();
  332. return output;
  333. }
  334. }
  335. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  336. public Color GetCommittedPixel(VecI posOnImage)
  337. {
  338. lock (lockObject)
  339. {
  340. ThrowIfDisposed();
  341. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  342. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  343. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
  344. {
  345. null => Colors.Transparent,
  346. var chunk => chunk.Surface.GetSrgbPixel(posInChunk)
  347. };
  348. }
  349. }
  350. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  351. public Color GetCommittedPixelRaw(VecI posOnImage)
  352. {
  353. lock (lockObject)
  354. {
  355. ThrowIfDisposed();
  356. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  357. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  358. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
  359. {
  360. null => Colors.Transparent,
  361. var chunk => chunk.Surface.GetRawPixel(posInChunk)
  362. };
  363. }
  364. }
  365. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  366. public Color GetMostUpToDatePixel(VecI posOnImage)
  367. {
  368. lock (lockObject)
  369. {
  370. ThrowIfDisposed();
  371. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  372. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  373. // nothing queued, return committed
  374. if (queuedOperations.Count == 0)
  375. {
  376. Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  377. return committedChunk switch
  378. {
  379. null => Colors.Transparent,
  380. _ => committedChunk.Surface.GetSrgbPixel(posInChunk)
  381. };
  382. }
  383. // something is queued, blend mode is Src so no merging needed
  384. if (blendMode == BlendMode.Src)
  385. {
  386. Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
  387. return latestChunk switch
  388. {
  389. null => Colors.Transparent,
  390. _ => latestChunk.Surface.GetSrgbPixel(posInChunk)
  391. };
  392. }
  393. // something is queued, blend mode is not Src so we have to do merging
  394. {
  395. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  396. Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  397. Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
  398. Color committedColor = committedChunk is null
  399. ? Colors.Transparent
  400. : committedChunk.Surface.GetSrgbPixel(posInChunk);
  401. Color latestColor = latestChunk is null
  402. ? Colors.Transparent
  403. : latestChunk.Surface.GetSrgbPixel(posInChunk);
  404. // using a whole chunk just to draw 1 pixel is kinda dumb,
  405. // but this should be faster than any approach that requires allocations
  406. using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
  407. using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
  408. using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
  409. tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecD(1)), committedPaint);
  410. tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecI(1)), latestPaint);
  411. return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
  412. }
  413. }
  414. }
  415. /// <returns>
  416. /// True if the chunk existed and was drawn, otherwise false
  417. /// </returns>
  418. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  419. public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos,
  420. Paint? paint = null, SamplingOptions? samplingOptions = null)
  421. {
  422. lock (lockObject)
  423. {
  424. ThrowIfDisposed();
  425. OneOf<None, EmptyChunk, Chunk> latestChunk;
  426. {
  427. var chunk = GetLatestChunk(chunkPos, resolution);
  428. if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
  429. {
  430. latestChunk = new EmptyChunk();
  431. }
  432. else
  433. {
  434. latestChunk = chunk is null ? new None() : chunk;
  435. }
  436. }
  437. var committedChunk = GetCommittedChunk(chunkPos, resolution);
  438. // draw committed directly
  439. if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
  440. {
  441. if (committedChunk is null)
  442. return false;
  443. committedChunk.DrawChunkOn(surface, pos, paint, samplingOptions);
  444. return true;
  445. }
  446. // no need to combine with committed, draw directly
  447. if (blendMode == BlendMode.Src || committedChunk is null)
  448. {
  449. if (latestChunk.IsT2)
  450. {
  451. latestChunk.AsT2.DrawChunkOn(surface, pos, paint, samplingOptions);
  452. return true;
  453. }
  454. return false;
  455. }
  456. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  457. // combine with committed and then draw
  458. using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
  459. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
  460. ReplacingPaint);
  461. blendModePaint.BlendMode = blendMode;
  462. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
  463. blendModePaint);
  464. if (lockTransparency)
  465. OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
  466. tempChunk.DrawChunkOn(surface, pos, paint, samplingOptions);
  467. return true;
  468. }
  469. }
  470. public bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface,
  471. VecD pos,
  472. Paint? paint = null, SamplingOptions? sampling = null)
  473. {
  474. lock (lockObject)
  475. {
  476. ThrowIfDisposed();
  477. OneOf<None, EmptyChunk, Chunk> latestChunk;
  478. {
  479. var chunk = MaybeGetLatestChunk(chunkPos, resolution);
  480. if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
  481. {
  482. latestChunk = new EmptyChunk();
  483. }
  484. else
  485. {
  486. latestChunk = chunk is null ? new None() : chunk;
  487. }
  488. }
  489. var committedChunk = GetCommittedChunk(chunkPos, resolution);
  490. // draw committed directly
  491. if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
  492. {
  493. if (committedChunk is null)
  494. return false;
  495. committedChunk.DrawChunkOn(surface, pos, paint, sampling);
  496. return true;
  497. }
  498. // no need to combine with committed, draw directly
  499. if (blendMode == BlendMode.Src || committedChunk is null)
  500. {
  501. if (latestChunk.IsT2)
  502. {
  503. latestChunk.AsT2.DrawChunkOn(surface, pos, paint, sampling);
  504. return true;
  505. }
  506. return false;
  507. }
  508. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  509. // combine with committed and then draw
  510. using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
  511. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
  512. ReplacingPaint);
  513. blendModePaint.BlendMode = blendMode;
  514. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
  515. blendModePaint);
  516. if (lockTransparency)
  517. OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
  518. tempChunk.DrawChunkOn(surface, pos, paint, sampling);
  519. return true;
  520. }
  521. }
  522. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  523. public bool LatestOrCommittedChunkExists(VecI chunkPos)
  524. {
  525. lock (lockObject)
  526. {
  527. ThrowIfDisposed();
  528. if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
  529. MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
  530. return true;
  531. foreach (var operation in queuedOperations)
  532. {
  533. if (operation.affectedArea.Chunks.Contains(chunkPos))
  534. return true;
  535. }
  536. return false;
  537. }
  538. }
  539. public bool LatestOrCommittedChunkExists()
  540. {
  541. lock (lockObject)
  542. {
  543. ThrowIfDisposed();
  544. var chunks = FindAllChunks();
  545. foreach (var chunk in chunks)
  546. {
  547. if (LatestOrCommittedChunkExists(chunk))
  548. return true;
  549. }
  550. }
  551. return false;
  552. }
  553. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  554. public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos,
  555. Paint? paint = null, SamplingOptions? samplingOptions = null)
  556. {
  557. lock (lockObject)
  558. {
  559. ThrowIfDisposed();
  560. var chunk = GetCommittedChunk(chunkPos, resolution);
  561. if (chunk is null)
  562. return false;
  563. chunk.DrawChunkOn(surface, pos, paint, samplingOptions);
  564. return true;
  565. }
  566. }
  567. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  568. internal bool CommittedChunkExists(VecI chunkPos)
  569. {
  570. lock (lockObject)
  571. {
  572. ThrowIfDisposed();
  573. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
  574. }
  575. }
  576. /// <summary>
  577. /// 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.
  578. /// </summary>
  579. private Chunk? GetLatestChunk(VecI pos, ChunkResolution resolution)
  580. {
  581. if (queuedOperations.Count == 0)
  582. return null;
  583. MaybeCreateAndProcessQueueForChunk(pos, resolution);
  584. var maybeNewlyProcessedChunk = MaybeGetLatestChunk(pos, resolution);
  585. return maybeNewlyProcessedChunk;
  586. }
  587. /// <summary>
  588. /// 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.
  589. private Chunk? GetCommittedChunk(VecI pos, ChunkResolution resolution)
  590. {
  591. var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
  592. if (maybeSameRes is not null)
  593. return maybeSameRes;
  594. var maybeFullRes = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
  595. if (maybeFullRes is not null)
  596. return GetOrCreateCommittedChunk(pos, resolution);
  597. return null;
  598. }
  599. private Chunk? MaybeGetLatestChunk(VecI pos, ChunkResolution resolution)
  600. => latestChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
  601. private Chunk? MaybeGetCommittedChunk(VecI pos, ChunkResolution resolution)
  602. => committedChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
  603. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  604. public void AddRasterClip(ChunkyImage clippingMask)
  605. {
  606. lock (lockObject)
  607. {
  608. ThrowIfDisposed();
  609. if (queuedOperations.Count > 0)
  610. throw new InvalidOperationException(
  611. "This function can only be executed when there are no queued operations");
  612. activeClips.Add(clippingMask);
  613. }
  614. }
  615. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  616. public void SetClippingPath(VectorPath clippingPath)
  617. {
  618. lock (lockObject)
  619. {
  620. ThrowIfDisposed();
  621. if (queuedOperations.Count > 0)
  622. throw new InvalidOperationException(
  623. "This function can only be executed when there are no queued operations");
  624. this.clippingPath = clippingPath;
  625. }
  626. }
  627. /// <summary>
  628. /// Porter duff compositing operators (apart from SrcOver) likely won't have the intended effect.
  629. /// </summary>
  630. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  631. public void SetBlendMode(BlendMode mode)
  632. {
  633. lock (lockObject)
  634. {
  635. ThrowIfDisposed();
  636. if (queuedOperations.Count > 0)
  637. throw new InvalidOperationException(
  638. "This function can only be executed when there are no queued operations");
  639. blendMode = mode;
  640. }
  641. }
  642. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  643. public void SetHorizontalAxisOfSymmetry(double position)
  644. {
  645. lock (lockObject)
  646. {
  647. ThrowIfDisposed();
  648. if (queuedOperations.Count > 0)
  649. throw new InvalidOperationException(
  650. "This function can only be executed when there are no queued operations");
  651. horizontalSymmetryAxis = position;
  652. }
  653. }
  654. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  655. public void SetVerticalAxisOfSymmetry(double position)
  656. {
  657. lock (lockObject)
  658. {
  659. ThrowIfDisposed();
  660. if (queuedOperations.Count > 0)
  661. throw new InvalidOperationException(
  662. "This function can only be executed when there are no queued operations");
  663. verticalSymmetryAxis = position;
  664. }
  665. }
  666. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  667. public void EnableLockTransparency()
  668. {
  669. lock (lockObject)
  670. {
  671. ThrowIfDisposed();
  672. lockTransparency = true;
  673. }
  674. }
  675. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  676. public void EnqueueReplaceColor(Color oldColor, Color newColor)
  677. {
  678. lock (lockObject)
  679. {
  680. ThrowIfDisposed();
  681. ReplaceColorOperation operation = new(oldColor, newColor);
  682. EnqueueOperation(operation);
  683. }
  684. }
  685. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  686. public void EnqueueDrawRectangle(ShapeData rect)
  687. {
  688. lock (lockObject)
  689. {
  690. ThrowIfDisposed();
  691. RectangleOperation operation = new(rect);
  692. EnqueueOperation(operation);
  693. }
  694. }
  695. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  696. public void EnqueueDrawEllipse(RectD location, Paintable? strokeColor, Paintable? fillColor, float strokeWidth,
  697. double rotationRad = 0, bool antiAliased = false,
  698. Paint? paint = null)
  699. {
  700. lock (lockObject)
  701. {
  702. ThrowIfDisposed();
  703. EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased,
  704. paint);
  705. EnqueueOperation(operation);
  706. }
  707. }
  708. /// <summary>
  709. /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
  710. /// It will however copy the surface right away which can be slow (in updateable changes especially).
  711. /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
  712. /// 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.
  713. /// </summary>
  714. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  715. public void EnqueueDrawImage(Matrix3X3 transformMatrix, Surface image, SamplingOptions samplingOptions, Paint? paint = null, bool copyImage = true)
  716. {
  717. lock (lockObject)
  718. {
  719. ThrowIfDisposed();
  720. ImageOperation operation = new(transformMatrix, image, samplingOptions, paint, copyImage);
  721. EnqueueOperation(operation);
  722. }
  723. }
  724. /// <summary>
  725. /// Be careful about the copyImage argument, see other overload for details
  726. /// </summary>
  727. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  728. public void EnqueueDrawImage(ShapeCorners corners, Surface image, Paint? paint = null, bool copyImage = true)
  729. {
  730. lock (lockObject)
  731. {
  732. ThrowIfDisposed();
  733. ImageOperation operation = new(corners, image, paint, copyImage);
  734. EnqueueOperation(operation);
  735. }
  736. }
  737. /// <summary>
  738. /// Be careful about the copyImage argument, see other overload for details
  739. /// </summary>
  740. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  741. public void EnqueueDrawImage(VecI pos, Surface image, Paint? paint = null, bool copyImage = true)
  742. {
  743. lock (lockObject)
  744. {
  745. ThrowIfDisposed();
  746. ImageOperation operation = new(pos, image, paint, copyImage);
  747. EnqueueOperation(operation);
  748. }
  749. }
  750. /// <summary>
  751. /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
  752. /// It will however copy the surface right away which can be slow (in updateable changes especially).
  753. /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
  754. /// 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.
  755. /// </summary>
  756. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  757. public void EnqueueDrawTexture(Matrix3X3 transformMatrix, Texture image, Paint? paint = null, bool copyImage = true)
  758. {
  759. lock (lockObject)
  760. {
  761. ThrowIfDisposed();
  762. TextureOperation operation = new(transformMatrix, image, paint, copyImage);
  763. EnqueueOperation(operation);
  764. }
  765. }
  766. /// <summary>
  767. /// Be careful about the copyImage argument, see other overload for details
  768. /// </summary>
  769. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  770. public void EnqueueDrawTexture(ShapeCorners corners, Texture image, Paint? paint = null, bool copyImage = true)
  771. {
  772. lock (lockObject)
  773. {
  774. ThrowIfDisposed();
  775. TextureOperation operation = new(corners, image, paint, copyImage);
  776. EnqueueOperation(operation);
  777. }
  778. }
  779. /// <summary>
  780. /// Be careful about the copyImage argument, see other overload for details
  781. /// </summary>
  782. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  783. public void EnqueueDrawTexture(VecI pos, Texture image, Paint? paint = null, bool copyImage = true)
  784. {
  785. lock (lockObject)
  786. {
  787. ThrowIfDisposed();
  788. TextureOperation operation = new(pos, image, paint, copyImage);
  789. EnqueueOperation(operation);
  790. }
  791. }
  792. public void EnqueueApplyMask(ChunkyImage mask)
  793. {
  794. lock (lockObject)
  795. {
  796. ThrowIfDisposed();
  797. ApplyMaskOperation operation = new(mask);
  798. EnqueueOperation(operation);
  799. }
  800. }
  801. /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
  802. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  803. public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap,
  804. BlendMode blendMode, RectI? customBounds = null)
  805. {
  806. lock (lockObject)
  807. {
  808. ThrowIfDisposed();
  809. PathOperation operation = new(path, color, strokeWidth, strokeCap, blendMode, customBounds);
  810. EnqueueOperation(operation);
  811. }
  812. }
  813. /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
  814. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  815. public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
  816. BlendMode blendMode, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
  817. {
  818. lock (lockObject)
  819. {
  820. ThrowIfDisposed();
  821. PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blendMode, style, antiAliasing,
  822. customBounds);
  823. EnqueueOperation(operation);
  824. }
  825. }
  826. /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
  827. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  828. public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
  829. Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
  830. {
  831. lock (lockObject)
  832. {
  833. ThrowIfDisposed();
  834. PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blender, style, antiAliasing,
  835. customBounds);
  836. EnqueueOperation(operation);
  837. }
  838. }
  839. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  840. public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
  841. {
  842. lock (lockObject)
  843. {
  844. ThrowIfDisposed();
  845. BresenhamLineOperation operation = new(from, to, paintable, blendMode);
  846. EnqueueOperation(operation);
  847. }
  848. }
  849. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  850. public void EnqueueDrawSkiaLine(VecD from, VecD to, StrokeCap strokeCap, float strokeWidth, Color color,
  851. BlendMode blendMode)
  852. {
  853. lock (lockObject)
  854. {
  855. ThrowIfDisposed();
  856. DrawingSurfaceLineOperation operation = new(from, to, strokeCap, strokeWidth, color, blendMode);
  857. EnqueueOperation(operation);
  858. }
  859. }
  860. public void EnqueueDrawSkiaLine(VecD from, VecD to, Paint paint)
  861. {
  862. lock (lockObject)
  863. {
  864. ThrowIfDisposed();
  865. DrawingSurfaceLineOperation operation = new(from, to, paint);
  866. EnqueueOperation(operation);
  867. }
  868. }
  869. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  870. public void EnqueueDrawPixels(IEnumerable<VecI> pixels, Color color, BlendMode blendMode)
  871. {
  872. lock (lockObject)
  873. {
  874. ThrowIfDisposed();
  875. PixelsOperation operation = new(pixels, color, blendMode);
  876. EnqueueOperation(operation);
  877. }
  878. }
  879. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  880. public void EnqueueDrawPixel(VecI pos, Color color, BlendMode blendMode)
  881. {
  882. lock (lockObject)
  883. {
  884. ThrowIfDisposed();
  885. PixelOperation operation = new(pos, color, blendMode);
  886. EnqueueOperation(operation);
  887. }
  888. }
  889. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  890. public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
  891. {
  892. lock (lockObject)
  893. {
  894. ThrowIfDisposed();
  895. ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, false);
  896. EnqueueOperation(operation);
  897. }
  898. }
  899. public void EnqueueDrawUpToDateChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
  900. {
  901. ThrowIfDisposed();
  902. ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, true);
  903. EnqueueOperation(operation);
  904. }
  905. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  906. public void EnqueueClearRegion(RectI region)
  907. {
  908. lock (lockObject)
  909. {
  910. ThrowIfDisposed();
  911. ClearRegionOperation operation = new(region);
  912. EnqueueOperation(operation);
  913. }
  914. }
  915. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  916. public void EnqueueClearPath(VectorPath path, RectI? pathTightBounds = null)
  917. {
  918. lock (lockObject)
  919. {
  920. ThrowIfDisposed();
  921. ClearPathOperation operation = new(path, pathTightBounds);
  922. EnqueueOperation(operation);
  923. }
  924. }
  925. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  926. public void EnqueueClear()
  927. {
  928. lock (lockObject)
  929. {
  930. ThrowIfDisposed();
  931. ClearOperation operation = new();
  932. EnqueueOperation(operation, new(FindAllChunks()));
  933. }
  934. }
  935. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  936. public void EnqueueResize(VecI newSize)
  937. {
  938. lock (lockObject)
  939. {
  940. ThrowIfDisposed();
  941. ResizeOperation operation = new(newSize);
  942. LatestSize = newSize;
  943. EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
  944. }
  945. }
  946. public void EnqueueDrawPaint(Paint paint)
  947. {
  948. lock (lockObject)
  949. {
  950. ThrowIfDisposed();
  951. PaintOperation operation = new(paint);
  952. EnqueueOperation(operation);
  953. }
  954. }
  955. private void EnqueueOperation(IDrawOperation operation)
  956. {
  957. List<IDrawOperation> operations = new(4) { operation };
  958. if (operation is IMirroredDrawOperation mirroredOperation)
  959. {
  960. if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
  961. operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
  962. if (horizontalSymmetryAxis is not null)
  963. operations.Add(mirroredOperation.AsMirrored(null, horizontalSymmetryAxis));
  964. if (verticalSymmetryAxis is not null)
  965. operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, null));
  966. }
  967. foreach (var op in operations)
  968. {
  969. var area = op.FindAffectedArea(LatestSize);
  970. area.Chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
  971. area.GlobalArea = area.GlobalArea?.Intersect(new RectI(VecI.Zero, LatestSize));
  972. if (operation.IgnoreEmptyChunks)
  973. area.Chunks.IntersectWith(FindAllChunks());
  974. EnqueueOperation(op, area);
  975. operationCounter++;
  976. }
  977. }
  978. private void EnqueueOperation(IOperation operation, AffectedArea area)
  979. {
  980. queuedOperations.Add((operation, area));
  981. }
  982. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  983. public void CancelChanges()
  984. {
  985. lock (lockObject)
  986. {
  987. ThrowIfDisposed();
  988. //clear queued operations
  989. foreach (var operation in queuedOperations)
  990. operation.operation.Dispose();
  991. queuedOperations.Clear();
  992. //clear additional state
  993. activeClips.Clear();
  994. blendMode = BlendMode.Src;
  995. lockTransparency = false;
  996. horizontalSymmetryAxis = null;
  997. verticalSymmetryAxis = null;
  998. clippingPath = null;
  999. //clear latest chunks
  1000. foreach (var (_, chunksOfRes) in latestChunks)
  1001. {
  1002. foreach (var (_, chunk) in chunksOfRes)
  1003. {
  1004. chunk.Dispose();
  1005. }
  1006. }
  1007. LatestSize = CommittedSize;
  1008. foreach (var (res, chunks) in latestChunks)
  1009. {
  1010. chunks.Clear();
  1011. latestChunksData[res].Clear();
  1012. }
  1013. }
  1014. }
  1015. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1016. public void CommitChanges()
  1017. {
  1018. lock (lockObject)
  1019. {
  1020. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  1021. ThrowIfDisposed();
  1022. var affectedArea = FindAffectedArea();
  1023. foreach (var chunk in affectedArea.Chunks)
  1024. {
  1025. MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
  1026. }
  1027. foreach (var (operation, _) in queuedOperations)
  1028. {
  1029. operation.Dispose();
  1030. }
  1031. CommitLatestChunks();
  1032. CommittedSize = LatestSize;
  1033. queuedOperations.Clear();
  1034. activeClips.Clear();
  1035. blendMode = BlendMode.Src;
  1036. lockTransparency = false;
  1037. horizontalSymmetryAxis = null;
  1038. verticalSymmetryAxis = null;
  1039. clippingPath = null;
  1040. commitCounter++;
  1041. if (commitCounter % 30 == 0)
  1042. FindAndDeleteEmptyCommittedChunks();
  1043. }
  1044. }
  1045. /// <summary>
  1046. /// Does all necessary steps to convert latest chunks into committed ones. The latest chunk dictionary become empty after this function is called.
  1047. /// </summary>
  1048. private void CommitLatestChunks()
  1049. {
  1050. // move/draw fully processed latest chunks to/on committed
  1051. foreach (var (resolution, chunks) in latestChunks)
  1052. {
  1053. foreach (var (pos, chunk) in chunks)
  1054. {
  1055. // get chunk if exists
  1056. LatestChunkData data = latestChunksData[resolution][pos];
  1057. if (data.QueueProgress != queuedOperations.Count)
  1058. {
  1059. if (resolution == ChunkResolution.Full)
  1060. {
  1061. throw new InvalidOperationException(
  1062. "Trying to commit a full res chunk that wasn't fully processed");
  1063. }
  1064. else
  1065. {
  1066. chunk.Dispose();
  1067. continue;
  1068. }
  1069. }
  1070. // do a swap
  1071. if (blendMode == BlendMode.Src)
  1072. {
  1073. // delete committed version
  1074. if (committedChunks[resolution].ContainsKey(pos))
  1075. {
  1076. var oldChunk = committedChunks[resolution][pos];
  1077. committedChunks[resolution].Remove(pos);
  1078. oldChunk.Dispose();
  1079. }
  1080. // put the latest version in place of the committed one
  1081. if (!data.IsDeleted)
  1082. committedChunks[resolution].Add(pos, chunk);
  1083. else
  1084. chunk.Dispose();
  1085. }
  1086. // do blending
  1087. else
  1088. {
  1089. // nothing to blend, continue
  1090. if (data.IsDeleted)
  1091. {
  1092. chunk.Dispose();
  1093. continue;
  1094. }
  1095. // nothing to blend with, swap
  1096. var maybeCommitted = MaybeGetCommittedChunk(pos, resolution);
  1097. if (maybeCommitted is null)
  1098. {
  1099. committedChunks[resolution].Add(pos, chunk);
  1100. continue;
  1101. }
  1102. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  1103. //blend
  1104. blendModePaint.BlendMode = blendMode;
  1105. if (lockTransparency)
  1106. {
  1107. using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1108. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0,
  1109. ReplacingPaint);
  1110. maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
  1111. blendModePaint);
  1112. OperationHelper.ClampAlpha(maybeCommitted.Surface.DrawingSurface,
  1113. tempChunk.Surface.DrawingSurface);
  1114. }
  1115. else
  1116. {
  1117. maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
  1118. blendModePaint);
  1119. }
  1120. chunk.Dispose();
  1121. }
  1122. }
  1123. }
  1124. // delete committed low res chunks that weren't updated
  1125. foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
  1126. {
  1127. foreach (var (resolution, _) in latestChunks)
  1128. {
  1129. if (resolution == ChunkResolution.Full)
  1130. continue;
  1131. if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) ||
  1132. halfChunk.QueueProgress != queuedOperations.Count)
  1133. {
  1134. if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
  1135. {
  1136. committedChunks[resolution].Remove(pos);
  1137. committedLowResChunk.Dispose();
  1138. }
  1139. }
  1140. }
  1141. }
  1142. // clear latest chunks
  1143. foreach (var (resolution, chunks) in latestChunks)
  1144. {
  1145. chunks.Clear();
  1146. latestChunksData[resolution].Clear();
  1147. }
  1148. }
  1149. /// <returns>
  1150. /// All chunks that have something in them, including latest (uncommitted) ones
  1151. /// </returns>
  1152. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1153. public HashSet<VecI> FindAllChunks()
  1154. {
  1155. lock (lockObject)
  1156. {
  1157. ThrowIfDisposed();
  1158. var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
  1159. foreach (var (_, affArea) in queuedOperations)
  1160. {
  1161. allChunks.UnionWith(affArea.Chunks);
  1162. }
  1163. return allChunks;
  1164. }
  1165. }
  1166. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1167. public HashSet<VecI> FindCommittedChunks()
  1168. {
  1169. lock (lockObject)
  1170. {
  1171. ThrowIfDisposed();
  1172. return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
  1173. }
  1174. }
  1175. public Dictionary<VecI, Surface> CloneAllCommitedNonEmptyChunks()
  1176. {
  1177. lock (lockObject)
  1178. {
  1179. ThrowIfDisposed();
  1180. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  1181. var dict = new Dictionary<VecI, Surface>();
  1182. foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
  1183. {
  1184. if (chunk.FindPreciseBounds().HasValue)
  1185. {
  1186. var surf = new Surface(chunk.Surface.ImageInfo);
  1187. surf.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0);
  1188. dict[pos] = surf;
  1189. surf.DrawingSurface.Flush();
  1190. }
  1191. }
  1192. return dict;
  1193. }
  1194. }
  1195. /// <returns>
  1196. /// Chunks affected by operations that haven't been committed yet
  1197. /// </returns>
  1198. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1199. public AffectedArea FindAffectedArea(int fromOperationIndex = 0)
  1200. {
  1201. lock (lockObject)
  1202. {
  1203. ThrowIfDisposed();
  1204. var chunks = new HashSet<VecI>();
  1205. RectI? rect = null;
  1206. for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
  1207. {
  1208. var (_, area) = queuedOperations[i];
  1209. chunks.UnionWith(area.Chunks);
  1210. rect ??= area.GlobalArea;
  1211. if (area.GlobalArea is not null && rect is not null)
  1212. rect = rect.Value.Union(area.GlobalArea.Value);
  1213. }
  1214. return new AffectedArea(chunks, rect);
  1215. }
  1216. }
  1217. public void SetCommitedChunk(Chunk chunk, VecI pos, ChunkResolution resolution)
  1218. {
  1219. lock (lockObject)
  1220. {
  1221. ThrowIfDisposed();
  1222. committedChunks[resolution][pos] = chunk;
  1223. }
  1224. }
  1225. /// <summary>
  1226. /// 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.
  1227. /// </summary>
  1228. private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
  1229. {
  1230. if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
  1231. chunkData = new()
  1232. {
  1233. QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos)
  1234. };
  1235. if (chunkData.QueueProgress == queuedOperations.Count)
  1236. return;
  1237. Chunk? targetChunk = null;
  1238. OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips = new FilledChunk();
  1239. bool initialized = false;
  1240. for (int i = 0; i < queuedOperations.Count; i++)
  1241. {
  1242. var (operation, affArea) = queuedOperations[i];
  1243. if (!affArea.Chunks.Contains(chunkPos))
  1244. continue;
  1245. if (!initialized)
  1246. {
  1247. initialized = true;
  1248. targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
  1249. combinedRasterClips = CombineRasterClipsForChunk(chunkPos, resolution);
  1250. }
  1251. if (chunkData.QueueProgress <= i)
  1252. chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!,
  1253. chunkPos, resolution, chunkData);
  1254. }
  1255. if (initialized)
  1256. {
  1257. if (lockTransparency && !chunkData.IsDeleted &&
  1258. MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
  1259. {
  1260. var committed = GetCommittedChunk(chunkPos, resolution);
  1261. OperationHelper.ClampAlpha(targetChunk!.Surface.DrawingSurface, committed!.Surface.DrawingSurface);
  1262. }
  1263. chunkData.QueueProgress = queuedOperations.Count;
  1264. latestChunksData[resolution][chunkPos] = chunkData;
  1265. }
  1266. if (combinedRasterClips.TryPickT2(out Chunk value, out var _))
  1267. value.Dispose();
  1268. }
  1269. private OneOf<FilledChunk, EmptyChunk, Chunk> CombineRasterClipsForChunk(VecI chunkPos, ChunkResolution resolution)
  1270. {
  1271. if (lockTransparency && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is null)
  1272. {
  1273. return new EmptyChunk();
  1274. }
  1275. if (activeClips.Count == 0)
  1276. {
  1277. return new FilledChunk();
  1278. }
  1279. var intersection = Chunk.Create(ProcessingColorSpace, resolution);
  1280. intersection.Surface.DrawingSurface.Canvas.Clear(Colors.White);
  1281. foreach (var mask in activeClips)
  1282. {
  1283. if (mask.CommittedChunkExists(chunkPos))
  1284. {
  1285. mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface.Canvas, VecI.Zero,
  1286. ClippingPaint);
  1287. }
  1288. else
  1289. {
  1290. intersection.Dispose();
  1291. return new EmptyChunk();
  1292. }
  1293. }
  1294. return intersection;
  1295. }
  1296. /// <returns>
  1297. /// True if the chunk was fully cleared (and should be deleted).
  1298. /// </returns>
  1299. private bool ApplyOperationToChunk(
  1300. IOperation operation,
  1301. AffectedArea operationAffectedArea,
  1302. OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
  1303. Chunk targetChunk,
  1304. VecI chunkPos,
  1305. ChunkResolution resolution,
  1306. LatestChunkData chunkData)
  1307. {
  1308. if (operation is ClearOperation)
  1309. return true;
  1310. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  1311. if (operation is IDrawOperation chunkOperation)
  1312. {
  1313. if (combinedRasterClips.IsT1) // Nothing is visible
  1314. return chunkData.IsDeleted;
  1315. if (chunkData.IsDeleted)
  1316. targetChunk.Surface.DrawingSurface.Canvas.Clear();
  1317. // just regular drawing
  1318. if (combinedRasterClips.IsT0) // Everything is visible as far as the raster clips are concerned
  1319. {
  1320. CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, targetChunk, resolution, chunkPos);
  1321. return false;
  1322. }
  1323. // drawing with raster clipping
  1324. var clip = combinedRasterClips.AsT2;
  1325. using var tempChunk = Chunk.Create(ProcessingColorSpace, targetChunk.Resolution);
  1326. targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface.Canvas, VecI.Zero, ReplacingPaint);
  1327. CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
  1328. clip.DrawChunkOn(tempChunk.Surface.DrawingSurface.Canvas, VecI.Zero, ClippingPaint);
  1329. clip.DrawChunkOn(targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, InverseClippingPaint);
  1330. tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, AddingPaint);
  1331. return false;
  1332. }
  1333. if (operation is ResizeOperation resizeOperation)
  1334. {
  1335. return IsOutsideBounds(chunkPos, resizeOperation.Size);
  1336. }
  1337. return chunkData.IsDeleted;
  1338. }
  1339. private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk,
  1340. ChunkResolution resolution, VecI chunkPos)
  1341. {
  1342. if (operationAffectedArea is null)
  1343. return;
  1344. int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
  1345. float scale = (float)resolution.Multiplier();
  1346. if (clippingPath is not null && !clippingPath.IsEmpty)
  1347. {
  1348. using VectorPath transformedPath = new(clippingPath);
  1349. VecD trans = -chunkPos * FullChunkSize * scale;
  1350. transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
  1351. targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
  1352. }
  1353. VecD affectedAreaPos = operationAffectedArea.Value.TopLeft;
  1354. VecD affectedAreaSize = operationAffectedArea.Value.Size;
  1355. affectedAreaPos = (affectedAreaPos - chunkPos * FullChunkSize) * scale;
  1356. affectedAreaSize = affectedAreaSize * scale;
  1357. targetChunk.Surface.DrawingSurface.Canvas.ClipRect(new RectD(affectedAreaPos, affectedAreaSize));
  1358. operation.DrawOnChunk(targetChunk, chunkPos);
  1359. targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
  1360. }
  1361. /// <summary>
  1362. /// Finds and deletes empty committed chunks. Returns true if all existing chunks were deleted.
  1363. /// 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).
  1364. /// </summary>
  1365. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1366. public bool CheckIfCommittedIsEmpty()
  1367. {
  1368. lock (lockObject)
  1369. {
  1370. ThrowIfDisposed();
  1371. if (queuedOperations.Count > 0)
  1372. throw new InvalidOperationException(
  1373. "This function can only be used when there are no queued operations");
  1374. FindAndDeleteEmptyCommittedChunks();
  1375. return committedChunks[ChunkResolution.Full].Count == 0;
  1376. }
  1377. }
  1378. private HashSet<VecI> FindAllChunksOutsideBounds(VecI size)
  1379. {
  1380. var chunks = FindAllChunks();
  1381. chunks.RemoveWhere(pos => !IsOutsideBounds(pos, size));
  1382. return chunks;
  1383. }
  1384. private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
  1385. {
  1386. return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X ||
  1387. chunkPos.Y * FullChunkSize >= imageSize.Y;
  1388. }
  1389. private void FindAndDeleteEmptyCommittedChunks()
  1390. {
  1391. if (queuedOperations.Count != 0)
  1392. throw new InvalidOperationException("This method cannot be used while any operations are queued");
  1393. HashSet<VecI> toRemove = new();
  1394. foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
  1395. {
  1396. if (chunk.Surface.IsFullyTransparent())
  1397. {
  1398. toRemove.Add(pos);
  1399. chunk.Dispose();
  1400. }
  1401. }
  1402. foreach (var pos in toRemove)
  1403. {
  1404. committedChunks[ChunkResolution.Full].Remove(pos);
  1405. committedChunks[ChunkResolution.Half].Remove(pos);
  1406. committedChunks[ChunkResolution.Quarter].Remove(pos);
  1407. committedChunks[ChunkResolution.Eighth].Remove(pos);
  1408. }
  1409. }
  1410. /// <summary>
  1411. /// Gets existing committed chunk or creates a new one. Doesn't apply any operations to the chunk, returns it as it is.
  1412. /// </summary>
  1413. private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
  1414. {
  1415. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  1416. // committed chunk of the same resolution exists
  1417. Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
  1418. if (targetChunk is not null)
  1419. return targetChunk;
  1420. // for full res chunks: nothing exists, create brand new chunk
  1421. if (resolution == ChunkResolution.Full)
  1422. {
  1423. var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1424. committedChunks[resolution][chunkPos] = newChunk;
  1425. return newChunk;
  1426. }
  1427. // for low res chunks: full res version exists
  1428. Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  1429. if (existingFullResChunk is not null)
  1430. {
  1431. var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1432. newChunk.Surface.DrawingSurface.Canvas.Save();
  1433. newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
  1434. using var snapshot = existingFullResChunk.Surface.DrawingSurface.Snapshot();
  1435. newChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, SamplingOptions.Bilinear,
  1436. SmoothReplacingPaint);
  1437. newChunk.Surface.DrawingSurface.Canvas.Restore();
  1438. committedChunks[resolution][chunkPos] = newChunk;
  1439. return newChunk;
  1440. }
  1441. // for low res chunks: full res version doesn't exist
  1442. {
  1443. GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
  1444. var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1445. committedChunks[resolution][chunkPos] = newChunk;
  1446. return newChunk;
  1447. }
  1448. }
  1449. /// <summary>
  1450. /// 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.
  1451. /// </summary>
  1452. private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
  1453. {
  1454. using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
  1455. // latest chunk exists
  1456. Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
  1457. if (targetChunk is not null)
  1458. return targetChunk;
  1459. // committed chunk of the same resolution exists
  1460. var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
  1461. if (maybeCommittedAnyRes is not null)
  1462. {
  1463. Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1464. if (blendMode == BlendMode.Src)
  1465. maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
  1466. else
  1467. newChunk.Surface.DrawingSurface.Canvas.Clear();
  1468. latestChunks[resolution][chunkPos] = newChunk;
  1469. return newChunk;
  1470. }
  1471. // committed chunk of full resolution exists
  1472. var maybeCommittedFullRes = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  1473. if (maybeCommittedFullRes is not null)
  1474. {
  1475. //create low res committed chunk
  1476. var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
  1477. //create latest based on it
  1478. Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1479. committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
  1480. latestChunks[resolution][chunkPos] = newChunk;
  1481. return newChunk;
  1482. }
  1483. // no previous chunks exist
  1484. var newLatestChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1485. newLatestChunk.Surface.DrawingSurface.Canvas.Clear();
  1486. latestChunks[resolution][chunkPos] = newLatestChunk;
  1487. return newLatestChunk;
  1488. }
  1489. private void ThrowIfDisposed()
  1490. {
  1491. if (disposed)
  1492. throw new ObjectDisposedException(nameof(ChunkyImage));
  1493. }
  1494. public void Dispose()
  1495. {
  1496. lock (lockObject)
  1497. {
  1498. if (disposed)
  1499. return;
  1500. CancelChanges();
  1501. DisposeAll();
  1502. blendModePaint.Dispose();
  1503. GC.SuppressFinalize(this);
  1504. }
  1505. }
  1506. private void DisposeAll()
  1507. {
  1508. foreach (var (_, chunks) in committedChunks)
  1509. {
  1510. foreach (var (_, chunk) in chunks)
  1511. {
  1512. chunk.Dispose();
  1513. }
  1514. }
  1515. foreach (var (_, chunks) in latestChunks)
  1516. {
  1517. foreach (var (_, chunk) in chunks)
  1518. {
  1519. chunk.Dispose();
  1520. }
  1521. }
  1522. disposed = true;
  1523. }
  1524. public object Clone()
  1525. {
  1526. lock (lockObject)
  1527. {
  1528. ThrowIfDisposed();
  1529. ChunkyImage clone = CloneFromCommitted();
  1530. return clone;
  1531. }
  1532. }
  1533. public int GetCacheHash()
  1534. {
  1535. HashCode hash = new HashCode();
  1536. hash.Add(commitCounter);
  1537. hash.Add(queuedOperations.Count);
  1538. hash.Add(operationCounter);
  1539. foreach (var queuedOperation in queuedOperations)
  1540. {
  1541. hash.Add(queuedOperation.affectedArea.GlobalArea?.GetHashCode() ?? 0);
  1542. hash.Add(queuedOperation.operation.GetHashCode());
  1543. }
  1544. hash.Add(activeClips.Count);
  1545. hash.Add((int)blendMode);
  1546. hash.Add(lockTransparency);
  1547. if (horizontalSymmetryAxis is not null)
  1548. hash.Add((int)(horizontalSymmetryAxis * 100));
  1549. if (verticalSymmetryAxis is not null)
  1550. hash.Add((int)(verticalSymmetryAxis * 100));
  1551. if (clippingPath is not null)
  1552. hash.Add(1);
  1553. return hash.ToHashCode();
  1554. }
  1555. }