ChunkyImage.cs 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  1. using System.ComponentModel.DataAnnotations;
  2. using System.Runtime.CompilerServices;
  3. using ChunkyImageLib.DataHolders;
  4. using ChunkyImageLib.Operations;
  5. using OneOf;
  6. using OneOf.Types;
  7. using PixiEditor.DrawingApi.Core.ColorsImpl;
  8. using PixiEditor.DrawingApi.Core.Numerics;
  9. using PixiEditor.DrawingApi.Core.Surface;
  10. using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
  11. using PixiEditor.DrawingApi.Core.Surface.Vector;
  12. [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
  13. namespace ChunkyImageLib;
  14. /// <summary>
  15. /// This class is thread-safe only for reading! Only the functions from IReadOnlyChunkyImage can be called from any thread.
  16. /// ChunkyImage can be in two general states:
  17. /// 1. a state with all chunks committed and no queued operations
  18. /// - latestChunks and latestChunksData are empty
  19. /// - queuedOperations are empty
  20. /// - committedChunks[ChunkResolution.Full] contains the current versions of all stored chunks
  21. /// - committedChunks[*any other resolution*] may contain the current low res versions of some of the chunks (or all of them, or none)
  22. /// - LatestSize == CommittedSize == current image size (px)
  23. /// 2. and a state with some queued operations
  24. /// - queuedOperations contains all requested operations (drawing, raster clips, clear, etc.)
  25. /// - committedChunks[ChunkResolution.Full] contains the last versions before any operations of all stored chunks
  26. /// - 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)
  27. /// - latestChunks stores chunks with some (or none, or all) queued operations applied
  28. /// - latestChunksData stores the data for some or all of the latest chunks (not necessarily synced with latestChunks).
  29. /// 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)
  30. /// - LatestSize contains the new size if any resize operations were requested, otherwise the committed size
  31. /// You can check the current state via queuedOperations.Count == 0
  32. ///
  33. /// Depending on the chosen blend mode the latest chunks contain different things:
  34. /// - BlendMode.Src: default mode, the latest chunks are the same as committed ones but with some or all queued operations applied.
  35. /// This means that operations can work with the existing pixels.
  36. /// - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
  37. /// 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.
  38. /// </summary>
  39. public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
  40. {
  41. private struct LatestChunkData
  42. {
  43. public LatestChunkData()
  44. {
  45. QueueProgress = 0;
  46. IsDeleted = false;
  47. }
  48. public int QueueProgress { get; set; }
  49. public bool IsDeleted { get; set; }
  50. }
  51. private bool disposed = false;
  52. private readonly object lockObject = new();
  53. private int commitCounter = 0;
  54. public const int FullChunkSize = ChunkPool.FullChunkSize;
  55. private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
  56. private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
  57. private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
  58. private static Paint SmoothReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
  59. private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
  60. private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
  61. public VecI CommittedSize { get; private set; }
  62. public VecI LatestSize { get; private set; }
  63. public int QueueLength
  64. {
  65. get
  66. {
  67. lock (lockObject)
  68. return queuedOperations.Count;
  69. }
  70. }
  71. private readonly List<(IOperation operation, AffectedArea affectedArea)> queuedOperations = new();
  72. private readonly List<ChunkyImage> activeClips = new();
  73. private BlendMode blendMode = BlendMode.Src;
  74. private bool lockTransparency = false;
  75. private VectorPath? clippingPath;
  76. private double? horizontalSymmetryAxis = null;
  77. private double? verticalSymmetryAxis = null;
  78. private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
  79. private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
  80. private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
  81. public ChunkyImage(VecI size)
  82. {
  83. CommittedSize = size;
  84. LatestSize = size;
  85. committedChunks = new()
  86. {
  87. [ChunkResolution.Full] = new(),
  88. [ChunkResolution.Half] = new(),
  89. [ChunkResolution.Quarter] = new(),
  90. [ChunkResolution.Eighth] = new(),
  91. };
  92. latestChunks = new()
  93. {
  94. [ChunkResolution.Full] = new(),
  95. [ChunkResolution.Half] = new(),
  96. [ChunkResolution.Quarter] = new(),
  97. [ChunkResolution.Eighth] = new(),
  98. };
  99. latestChunksData = new()
  100. {
  101. [ChunkResolution.Full] = new(),
  102. [ChunkResolution.Half] = new(),
  103. [ChunkResolution.Quarter] = new(),
  104. [ChunkResolution.Eighth] = new(),
  105. };
  106. }
  107. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  108. public RectI? FindLatestBounds()
  109. {
  110. lock (lockObject)
  111. {
  112. ThrowIfDisposed();
  113. RectI? rect = null;
  114. foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
  115. {
  116. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  117. rect ??= chunkBounds;
  118. rect = rect.Value.Union(chunkBounds);
  119. }
  120. foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
  121. {
  122. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  123. rect ??= chunkBounds;
  124. rect = rect.Value.Union(chunkBounds);
  125. }
  126. return rect;
  127. }
  128. }
  129. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  130. public RectI? FindPreciseCommittedBounds()
  131. {
  132. lock (lockObject)
  133. {
  134. ThrowIfDisposed();
  135. RectI? preciseBounds = null;
  136. foreach (var (chunkPos, chunk) in committedChunks[ChunkResolution.Full])
  137. {
  138. RectI? chunkPreciseBounds = chunk.FindPreciseBounds();
  139. if(chunkPreciseBounds is null)
  140. continue;
  141. RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * FullChunkSize);
  142. preciseBounds ??= globalChunkBounds;
  143. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  144. }
  145. preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
  146. return preciseBounds;
  147. }
  148. }
  149. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  150. public ChunkyImage CloneFromCommitted()
  151. {
  152. lock (lockObject)
  153. {
  154. ThrowIfDisposed();
  155. ChunkyImage output = new(LatestSize);
  156. var chunks = FindCommittedChunks();
  157. foreach (var chunk in chunks)
  158. {
  159. var image = GetCommittedChunk(chunk, ChunkResolution.Full);
  160. if (image is null)
  161. continue;
  162. output.EnqueueDrawImage(chunk * FullChunkSize, image.Surface);
  163. }
  164. output.CommitChanges();
  165. return output;
  166. }
  167. }
  168. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  169. public Color GetCommittedPixel(VecI posOnImage)
  170. {
  171. lock (lockObject)
  172. {
  173. ThrowIfDisposed();
  174. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  175. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  176. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
  177. {
  178. null => Colors.Transparent,
  179. var chunk => chunk.Surface.GetSRGBPixel(posInChunk)
  180. };
  181. }
  182. }
  183. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  184. public Color GetMostUpToDatePixel(VecI posOnImage)
  185. {
  186. lock (lockObject)
  187. {
  188. ThrowIfDisposed();
  189. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  190. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  191. // nothing queued, return committed
  192. if (queuedOperations.Count == 0)
  193. {
  194. Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  195. return committedChunk switch
  196. {
  197. null => Colors.Transparent,
  198. _ => committedChunk.Surface.GetSRGBPixel(posInChunk)
  199. };
  200. }
  201. // something is queued, blend mode is Src so no merging needed
  202. if (blendMode == BlendMode.Src)
  203. {
  204. Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
  205. return latestChunk switch
  206. {
  207. null => Colors.Transparent,
  208. _ => latestChunk.Surface.GetSRGBPixel(posInChunk)
  209. };
  210. }
  211. // something is queued, blend mode is not Src so we have to do merging
  212. {
  213. Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  214. Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
  215. Color committedColor = committedChunk is null ?
  216. Colors.Transparent :
  217. committedChunk.Surface.GetSRGBPixel(posInChunk);
  218. Color latestColor = latestChunk is null ?
  219. Colors.Transparent :
  220. latestChunk.Surface.GetSRGBPixel(posInChunk);
  221. // using a whole chunk just to draw 1 pixel is kinda dumb,
  222. // but this should be faster than any approach that requires allocations
  223. using Chunk tempChunk = Chunk.Create(ChunkResolution.Eighth);
  224. using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
  225. using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
  226. tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, committedPaint);
  227. tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, latestPaint);
  228. return tempChunk.Surface.GetSRGBPixel(VecI.Zero);
  229. }
  230. }
  231. }
  232. /// <returns>
  233. /// True if the chunk existed and was drawn, otherwise false
  234. /// </returns>
  235. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  236. public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null)
  237. {
  238. lock (lockObject)
  239. {
  240. ThrowIfDisposed();
  241. OneOf<None, EmptyChunk, Chunk> latestChunk;
  242. {
  243. var chunk = GetLatestChunk(chunkPos, resolution);
  244. if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
  245. {
  246. latestChunk = new EmptyChunk();
  247. }
  248. else
  249. {
  250. latestChunk = chunk is null ? new None() : chunk;
  251. }
  252. }
  253. var committedChunk = GetCommittedChunk(chunkPos, resolution);
  254. // draw committed directly
  255. if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
  256. {
  257. if (committedChunk is null)
  258. return false;
  259. committedChunk.DrawOnSurface(surface, pos, paint);
  260. return true;
  261. }
  262. // no need to combine with committed, draw directly
  263. if (blendMode == BlendMode.Src || committedChunk is null)
  264. {
  265. if (latestChunk.IsT2)
  266. {
  267. latestChunk.AsT2.DrawOnSurface(surface, pos, paint);
  268. return true;
  269. }
  270. return false;
  271. }
  272. // combine with committed and then draw
  273. using var tempChunk = Chunk.Create(resolution);
  274. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0, ReplacingPaint);
  275. blendModePaint.BlendMode = blendMode;
  276. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0, blendModePaint);
  277. if (lockTransparency)
  278. OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
  279. tempChunk.DrawOnSurface(surface, pos, paint);
  280. return true;
  281. }
  282. }
  283. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  284. public bool LatestOrCommittedChunkExists(VecI chunkPos)
  285. {
  286. lock (lockObject)
  287. {
  288. ThrowIfDisposed();
  289. if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
  290. MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
  291. return true;
  292. foreach (var operation in queuedOperations)
  293. {
  294. if (operation.affectedArea.Chunks.Contains(chunkPos))
  295. return true;
  296. }
  297. return false;
  298. }
  299. }
  300. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  301. public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null)
  302. {
  303. lock (lockObject)
  304. {
  305. ThrowIfDisposed();
  306. var chunk = GetCommittedChunk(chunkPos, resolution);
  307. if (chunk is null)
  308. return false;
  309. chunk.DrawOnSurface(surface, pos, paint);
  310. return true;
  311. }
  312. }
  313. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  314. internal bool CommittedChunkExists(VecI chunkPos)
  315. {
  316. lock (lockObject)
  317. {
  318. ThrowIfDisposed();
  319. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
  320. }
  321. }
  322. /// <summary>
  323. /// 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.
  324. /// </summary>
  325. private Chunk? GetLatestChunk(VecI pos, ChunkResolution resolution)
  326. {
  327. if (queuedOperations.Count == 0)
  328. return null;
  329. MaybeCreateAndProcessQueueForChunk(pos, resolution);
  330. var maybeNewlyProcessedChunk = MaybeGetLatestChunk(pos, resolution);
  331. return maybeNewlyProcessedChunk;
  332. }
  333. /// <summary>
  334. /// 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.
  335. /// </summary>
  336. private Chunk? GetCommittedChunk(VecI pos, ChunkResolution resolution)
  337. {
  338. var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
  339. if (maybeSameRes is not null)
  340. return maybeSameRes;
  341. var maybeFullRes = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
  342. if (maybeFullRes is not null)
  343. return GetOrCreateCommittedChunk(pos, resolution);
  344. return null;
  345. }
  346. private Chunk? MaybeGetLatestChunk(VecI pos, ChunkResolution resolution)
  347. => latestChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
  348. private Chunk? MaybeGetCommittedChunk(VecI pos, ChunkResolution resolution)
  349. => committedChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
  350. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  351. public void AddRasterClip(ChunkyImage clippingMask)
  352. {
  353. lock (lockObject)
  354. {
  355. ThrowIfDisposed();
  356. if (queuedOperations.Count > 0)
  357. throw new InvalidOperationException("This function can only be executed when there are no queued operations");
  358. activeClips.Add(clippingMask);
  359. }
  360. }
  361. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  362. public void SetClippingPath(VectorPath clippingPath)
  363. {
  364. lock (lockObject)
  365. {
  366. ThrowIfDisposed();
  367. if (queuedOperations.Count > 0)
  368. throw new InvalidOperationException("This function can only be executed when there are no queued operations");
  369. this.clippingPath = clippingPath;
  370. }
  371. }
  372. /// <summary>
  373. /// Porter duff compositing operators (apart from SrcOver) likely won't have the intended effect.
  374. /// </summary>
  375. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  376. public void SetBlendMode(BlendMode mode)
  377. {
  378. lock (lockObject)
  379. {
  380. ThrowIfDisposed();
  381. if (queuedOperations.Count > 0)
  382. throw new InvalidOperationException("This function can only be executed when there are no queued operations");
  383. blendMode = mode;
  384. }
  385. }
  386. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  387. public void SetHorizontalAxisOfSymmetry(double position)
  388. {
  389. lock (lockObject)
  390. {
  391. ThrowIfDisposed();
  392. if (queuedOperations.Count > 0)
  393. throw new InvalidOperationException("This function can only be executed when there are no queued operations");
  394. horizontalSymmetryAxis = position;
  395. }
  396. }
  397. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  398. public void SetVerticalAxisOfSymmetry(double position)
  399. {
  400. lock (lockObject)
  401. {
  402. ThrowIfDisposed();
  403. if (queuedOperations.Count > 0)
  404. throw new InvalidOperationException("This function can only be executed when there are no queued operations");
  405. verticalSymmetryAxis = position;
  406. }
  407. }
  408. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  409. public void EnableLockTransparency()
  410. {
  411. lock (lockObject)
  412. {
  413. ThrowIfDisposed();
  414. lockTransparency = true;
  415. }
  416. }
  417. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  418. public void EnqueueReplaceColor(Color oldColor, Color newColor)
  419. {
  420. lock (lockObject)
  421. {
  422. ThrowIfDisposed();
  423. ReplaceColorOperation operation = new(oldColor, newColor);
  424. EnqueueOperation(operation);
  425. }
  426. }
  427. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  428. public void EnqueueDrawRectangle(ShapeData rect)
  429. {
  430. lock (lockObject)
  431. {
  432. ThrowIfDisposed();
  433. RectangleOperation operation = new(rect);
  434. EnqueueOperation(operation);
  435. }
  436. }
  437. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  438. public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth, Paint? paint = null)
  439. {
  440. lock (lockObject)
  441. {
  442. ThrowIfDisposed();
  443. EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, paint);
  444. EnqueueOperation(operation);
  445. }
  446. }
  447. /// <summary>
  448. /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
  449. /// It will however copy the surface right away which can be slow (in updateable changes especially).
  450. /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
  451. /// 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.
  452. /// </summary>
  453. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  454. public void EnqueueDrawImage(Matrix3X3 transformMatrix, Surface image, Paint? paint = null, bool copyImage = true)
  455. {
  456. lock (lockObject)
  457. {
  458. ThrowIfDisposed();
  459. ImageOperation operation = new(transformMatrix, image, paint, copyImage);
  460. EnqueueOperation(operation);
  461. }
  462. }
  463. /// <summary>
  464. /// Be careful about the copyImage argument, see other overload for details
  465. /// </summary>
  466. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  467. public void EnqueueDrawImage(ShapeCorners corners, Surface image, Paint? paint = null, bool copyImage = true)
  468. {
  469. lock (lockObject)
  470. {
  471. ThrowIfDisposed();
  472. ImageOperation operation = new(corners, image, paint, copyImage);
  473. EnqueueOperation(operation);
  474. }
  475. }
  476. /// <summary>
  477. /// Be careful about the copyImage argument, see other overload for details
  478. /// </summary>
  479. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  480. public void EnqueueDrawImage(VecI pos, Surface image, Paint? paint = null, bool copyImage = true)
  481. {
  482. lock (lockObject)
  483. {
  484. ThrowIfDisposed();
  485. ImageOperation operation = new(pos, image, paint, copyImage);
  486. EnqueueOperation(operation);
  487. }
  488. }
  489. public void EnqueueApplyMask(ChunkyImage mask)
  490. {
  491. lock (lockObject)
  492. {
  493. ThrowIfDisposed();
  494. ApplyMaskOperation operation = new(mask);
  495. EnqueueOperation(operation);
  496. }
  497. }
  498. /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
  499. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  500. public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap, BlendMode blendMode, RectI? customBounds = null)
  501. {
  502. lock (lockObject)
  503. {
  504. ThrowIfDisposed();
  505. PathOperation operation = new(path, color, strokeWidth, strokeCap, blendMode, customBounds);
  506. EnqueueOperation(operation);
  507. }
  508. }
  509. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  510. public void EnqueueDrawBresenhamLine(VecI from, VecI to, Color color, BlendMode blendMode)
  511. {
  512. lock (lockObject)
  513. {
  514. ThrowIfDisposed();
  515. BresenhamLineOperation operation = new(from, to, color, blendMode);
  516. EnqueueOperation(operation);
  517. }
  518. }
  519. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  520. public void EnqueueDrawSkiaLine(VecI from, VecI to, StrokeCap strokeCap, float strokeWidth, Color color, BlendMode blendMode)
  521. {
  522. lock (lockObject)
  523. {
  524. ThrowIfDisposed();
  525. DrawingSurfaceLineOperation operation = new(from, to, strokeCap, strokeWidth, color, blendMode);
  526. EnqueueOperation(operation);
  527. }
  528. }
  529. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  530. public void EnqueueDrawPixels(IEnumerable<VecI> pixels, Color color, BlendMode blendMode)
  531. {
  532. lock (lockObject)
  533. {
  534. ThrowIfDisposed();
  535. PixelsOperation operation = new(pixels, color, blendMode);
  536. EnqueueOperation(operation);
  537. }
  538. }
  539. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  540. public void EnqueueDrawPixel(VecI pos, Color color, BlendMode blendMode)
  541. {
  542. lock (lockObject)
  543. {
  544. ThrowIfDisposed();
  545. PixelOperation operation = new(pos, color, blendMode);
  546. EnqueueOperation(operation);
  547. }
  548. }
  549. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  550. public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
  551. {
  552. lock (lockObject)
  553. {
  554. ThrowIfDisposed();
  555. PixelOperation operation = new(pos, pixelProcessor, blendMode);
  556. EnqueueOperation(operation);
  557. }
  558. }
  559. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  560. public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
  561. {
  562. lock (lockObject)
  563. {
  564. ThrowIfDisposed();
  565. ChunkyImageOperation operation = new(image, pos, flipHor, flipVer);
  566. EnqueueOperation(operation);
  567. }
  568. }
  569. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  570. public void EnqueueClearRegion(RectI region)
  571. {
  572. lock (lockObject)
  573. {
  574. ThrowIfDisposed();
  575. ClearRegionOperation operation = new(region);
  576. EnqueueOperation(operation);
  577. }
  578. }
  579. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  580. public void EnqueueClearPath(VectorPath path, RectI? pathTightBounds = null)
  581. {
  582. lock (lockObject)
  583. {
  584. ThrowIfDisposed();
  585. ClearPathOperation operation = new(path, pathTightBounds);
  586. EnqueueOperation(operation);
  587. }
  588. }
  589. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  590. public void EnqueueClear()
  591. {
  592. lock (lockObject)
  593. {
  594. ThrowIfDisposed();
  595. ClearOperation operation = new();
  596. EnqueueOperation(operation, new(FindAllChunks()));
  597. }
  598. }
  599. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  600. public void EnqueueResize(VecI newSize)
  601. {
  602. lock (lockObject)
  603. {
  604. ThrowIfDisposed();
  605. ResizeOperation operation = new(newSize);
  606. LatestSize = newSize;
  607. EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
  608. }
  609. }
  610. private void EnqueueOperation(IDrawOperation operation)
  611. {
  612. List<IDrawOperation> operations = new(4) { operation };
  613. if (operation is IMirroredDrawOperation mirroredOperation)
  614. {
  615. if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
  616. operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
  617. if (horizontalSymmetryAxis is not null)
  618. operations.Add(mirroredOperation.AsMirrored(null, horizontalSymmetryAxis));
  619. if (verticalSymmetryAxis is not null)
  620. operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, null));
  621. }
  622. foreach (var op in operations)
  623. {
  624. var area = op.FindAffectedArea(LatestSize);
  625. area.Chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
  626. area.GlobalArea = area.GlobalArea?.Intersect(new RectI(VecI.Zero, LatestSize));
  627. if (operation.IgnoreEmptyChunks)
  628. area.Chunks.IntersectWith(FindAllChunks());
  629. EnqueueOperation(op, area);
  630. }
  631. }
  632. private void EnqueueOperation(IOperation operation, AffectedArea area)
  633. {
  634. queuedOperations.Add((operation, area));
  635. }
  636. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  637. public void CancelChanges()
  638. {
  639. lock (lockObject)
  640. {
  641. ThrowIfDisposed();
  642. //clear queued operations
  643. foreach (var operation in queuedOperations)
  644. operation.operation.Dispose();
  645. queuedOperations.Clear();
  646. //clear additional state
  647. activeClips.Clear();
  648. blendMode = BlendMode.Src;
  649. lockTransparency = false;
  650. horizontalSymmetryAxis = null;
  651. verticalSymmetryAxis = null;
  652. clippingPath = null;
  653. //clear latest chunks
  654. foreach (var (_, chunksOfRes) in latestChunks)
  655. {
  656. foreach (var (_, chunk) in chunksOfRes)
  657. {
  658. chunk.Dispose();
  659. }
  660. }
  661. LatestSize = CommittedSize;
  662. foreach (var (res, chunks) in latestChunks)
  663. {
  664. chunks.Clear();
  665. latestChunksData[res].Clear();
  666. }
  667. }
  668. }
  669. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  670. public void CommitChanges()
  671. {
  672. lock (lockObject)
  673. {
  674. ThrowIfDisposed();
  675. var affectedArea = FindAffectedArea();
  676. foreach (var chunk in affectedArea.Chunks)
  677. {
  678. MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
  679. }
  680. foreach (var (operation, _) in queuedOperations)
  681. {
  682. operation.Dispose();
  683. }
  684. CommitLatestChunks();
  685. CommittedSize = LatestSize;
  686. queuedOperations.Clear();
  687. activeClips.Clear();
  688. blendMode = BlendMode.Src;
  689. lockTransparency = false;
  690. horizontalSymmetryAxis = null;
  691. verticalSymmetryAxis = null;
  692. clippingPath = null;
  693. commitCounter++;
  694. if (commitCounter % 30 == 0)
  695. FindAndDeleteEmptyCommittedChunks();
  696. }
  697. }
  698. /// <summary>
  699. /// Does all necessary steps to convert latest chunks into committed ones. The latest chunk dictionary become empty after this function is called.
  700. /// </summary>
  701. private void CommitLatestChunks()
  702. {
  703. // move/draw fully processed latest chunks to/on committed
  704. foreach (var (resolution, chunks) in latestChunks)
  705. {
  706. foreach (var (pos, chunk) in chunks)
  707. {
  708. // get chunk if exists
  709. LatestChunkData data = latestChunksData[resolution][pos];
  710. if (data.QueueProgress != queuedOperations.Count)
  711. {
  712. if (resolution == ChunkResolution.Full)
  713. {
  714. throw new InvalidOperationException("Trying to commit a full res chunk that wasn't fully processed");
  715. }
  716. else
  717. {
  718. chunk.Dispose();
  719. continue;
  720. }
  721. }
  722. // do a swap
  723. if (blendMode == BlendMode.Src)
  724. {
  725. // delete committed version
  726. if (committedChunks[resolution].ContainsKey(pos))
  727. {
  728. var oldChunk = committedChunks[resolution][pos];
  729. committedChunks[resolution].Remove(pos);
  730. oldChunk.Dispose();
  731. }
  732. // put the latest version in place of the committed one
  733. if (!data.IsDeleted)
  734. committedChunks[resolution].Add(pos, chunk);
  735. else
  736. chunk.Dispose();
  737. }
  738. // do blending
  739. else
  740. {
  741. // nothing to blend, continue
  742. if (data.IsDeleted)
  743. {
  744. chunk.Dispose();
  745. continue;
  746. }
  747. // nothing to blend with, swap
  748. var maybeCommitted = MaybeGetCommittedChunk(pos, resolution);
  749. if (maybeCommitted is null)
  750. {
  751. committedChunks[resolution].Add(pos, chunk);
  752. continue;
  753. }
  754. //blend
  755. blendModePaint.BlendMode = blendMode;
  756. if (lockTransparency)
  757. {
  758. using Chunk tempChunk = Chunk.Create(resolution);
  759. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0, ReplacingPaint);
  760. maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0, blendModePaint);
  761. OperationHelper.ClampAlpha(maybeCommitted.Surface.DrawingSurface, tempChunk.Surface.DrawingSurface);
  762. }
  763. else
  764. {
  765. maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0, blendModePaint);
  766. }
  767. chunk.Dispose();
  768. }
  769. }
  770. }
  771. // delete committed low res chunks that weren't updated
  772. foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
  773. {
  774. foreach (var (resolution, _) in latestChunks)
  775. {
  776. if (resolution == ChunkResolution.Full)
  777. continue;
  778. if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) || halfChunk.QueueProgress != queuedOperations.Count)
  779. {
  780. if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
  781. {
  782. committedChunks[resolution].Remove(pos);
  783. committedLowResChunk.Dispose();
  784. }
  785. }
  786. }
  787. }
  788. // clear latest chunks
  789. foreach (var (resolution, chunks) in latestChunks)
  790. {
  791. chunks.Clear();
  792. latestChunksData[resolution].Clear();
  793. }
  794. }
  795. /// <returns>
  796. /// All chunks that have something in them, including latest (uncommitted) ones
  797. /// </returns>
  798. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  799. public HashSet<VecI> FindAllChunks()
  800. {
  801. lock (lockObject)
  802. {
  803. ThrowIfDisposed();
  804. var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
  805. foreach (var (_, affArea) in queuedOperations)
  806. {
  807. allChunks.UnionWith(affArea.Chunks);
  808. }
  809. return allChunks;
  810. }
  811. }
  812. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  813. public HashSet<VecI> FindCommittedChunks()
  814. {
  815. lock (lockObject)
  816. {
  817. ThrowIfDisposed();
  818. return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
  819. }
  820. }
  821. /// <returns>
  822. /// Chunks affected by operations that haven't been committed yet
  823. /// </returns>
  824. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  825. public AffectedArea FindAffectedArea(int fromOperationIndex = 0)
  826. {
  827. lock (lockObject)
  828. {
  829. ThrowIfDisposed();
  830. var chunks = new HashSet<VecI>();
  831. RectI? rect = null;
  832. for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
  833. {
  834. var (_, area) = queuedOperations[i];
  835. chunks.UnionWith(area.Chunks);
  836. rect ??= area.GlobalArea;
  837. if (area.GlobalArea is not null && rect is not null)
  838. rect = rect.Value.Union(area.GlobalArea.Value);
  839. }
  840. return new AffectedArea(chunks, rect);
  841. }
  842. }
  843. /// <summary>
  844. /// 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.
  845. /// </summary>
  846. private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
  847. {
  848. if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
  849. chunkData = new() { QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos) };
  850. if (chunkData.QueueProgress == queuedOperations.Count)
  851. return;
  852. Chunk? targetChunk = null;
  853. OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips = new FilledChunk();
  854. bool initialized = false;
  855. for (int i = 0; i < queuedOperations.Count; i++)
  856. {
  857. var (operation, affArea) = queuedOperations[i];
  858. if (!affArea.Chunks.Contains(chunkPos))
  859. continue;
  860. if (!initialized)
  861. {
  862. initialized = true;
  863. targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
  864. combinedRasterClips = CombineRasterClipsForChunk(chunkPos, resolution);
  865. }
  866. if (chunkData.QueueProgress <= i)
  867. chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
  868. }
  869. if (initialized)
  870. {
  871. if (lockTransparency && !chunkData.IsDeleted && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
  872. {
  873. var committed = GetCommittedChunk(chunkPos, resolution);
  874. OperationHelper.ClampAlpha(targetChunk!.Surface.DrawingSurface, committed!.Surface.DrawingSurface);
  875. }
  876. chunkData.QueueProgress = queuedOperations.Count;
  877. latestChunksData[resolution][chunkPos] = chunkData;
  878. }
  879. if (combinedRasterClips.TryPickT2(out Chunk value, out var _))
  880. value.Dispose();
  881. }
  882. private OneOf<FilledChunk, EmptyChunk, Chunk> CombineRasterClipsForChunk(VecI chunkPos, ChunkResolution resolution)
  883. {
  884. if (lockTransparency && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is null)
  885. {
  886. return new EmptyChunk();
  887. }
  888. if (activeClips.Count == 0)
  889. {
  890. return new FilledChunk();
  891. }
  892. var intersection = Chunk.Create(resolution);
  893. intersection.Surface.DrawingSurface.Canvas.Clear(Colors.White);
  894. foreach (var mask in activeClips)
  895. {
  896. if (mask.CommittedChunkExists(chunkPos))
  897. {
  898. mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
  899. }
  900. else
  901. {
  902. intersection.Dispose();
  903. return new EmptyChunk();
  904. }
  905. }
  906. return intersection;
  907. }
  908. /// <returns>
  909. /// True if the chunk was fully cleared (and should be deleted).
  910. /// </returns>
  911. private bool ApplyOperationToChunk(
  912. IOperation operation,
  913. AffectedArea operationAffectedArea,
  914. OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
  915. Chunk targetChunk,
  916. VecI chunkPos,
  917. ChunkResolution resolution,
  918. LatestChunkData chunkData)
  919. {
  920. if (operation is ClearOperation)
  921. return true;
  922. if (operation is IDrawOperation chunkOperation)
  923. {
  924. if (combinedRasterClips.IsT1) // Nothing is visible
  925. return chunkData.IsDeleted;
  926. if (chunkData.IsDeleted)
  927. targetChunk.Surface.DrawingSurface.Canvas.Clear();
  928. // just regular drawing
  929. if (combinedRasterClips.IsT0) // Everything is visible as far as the raster clips are concerned
  930. {
  931. CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, targetChunk, resolution, chunkPos);
  932. return false;
  933. }
  934. // drawing with raster clipping
  935. var clip = combinedRasterClips.AsT2;
  936. using var tempChunk = Chunk.Create(targetChunk.Resolution);
  937. targetChunk.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
  938. CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
  939. clip.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
  940. clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
  941. tempChunk.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
  942. return false;
  943. }
  944. if (operation is ResizeOperation resizeOperation)
  945. {
  946. return IsOutsideBounds(chunkPos, resizeOperation.Size);
  947. }
  948. return chunkData.IsDeleted;
  949. }
  950. private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
  951. {
  952. if (operationAffectedArea is null)
  953. return;
  954. int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
  955. float scale = (float)resolution.Multiplier();
  956. if (clippingPath is not null && !clippingPath.IsEmpty)
  957. {
  958. using VectorPath transformedPath = new(clippingPath);
  959. VecD trans = -chunkPos * FullChunkSize * scale;
  960. transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
  961. targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
  962. }
  963. VecD affectedAreaPos = operationAffectedArea.Value.TopLeft;
  964. VecD affectedAreaSize = operationAffectedArea.Value.Size;
  965. affectedAreaPos = (affectedAreaPos - chunkPos * FullChunkSize) * scale;
  966. affectedAreaSize = affectedAreaSize * scale;
  967. targetChunk.Surface.DrawingSurface.Canvas.ClipRect(new RectD(affectedAreaPos, affectedAreaSize));
  968. operation.DrawOnChunk(targetChunk, chunkPos);
  969. targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
  970. }
  971. /// <summary>
  972. /// Finds and deletes empty committed chunks. Returns true if all existing chunks were deleted.
  973. /// 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).
  974. /// </summary>
  975. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  976. public bool CheckIfCommittedIsEmpty()
  977. {
  978. lock (lockObject)
  979. {
  980. ThrowIfDisposed();
  981. if (queuedOperations.Count > 0)
  982. throw new InvalidOperationException("This function can only be used when there are no queued operations");
  983. FindAndDeleteEmptyCommittedChunks();
  984. return committedChunks[ChunkResolution.Full].Count == 0;
  985. }
  986. }
  987. private HashSet<VecI> FindAllChunksOutsideBounds(VecI size)
  988. {
  989. var chunks = FindAllChunks();
  990. chunks.RemoveWhere(pos => !IsOutsideBounds(pos, size));
  991. return chunks;
  992. }
  993. private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
  994. {
  995. return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X || chunkPos.Y * FullChunkSize >= imageSize.Y;
  996. }
  997. private void FindAndDeleteEmptyCommittedChunks()
  998. {
  999. if (queuedOperations.Count != 0)
  1000. throw new InvalidOperationException("This method cannot be used while any operations are queued");
  1001. HashSet<VecI> toRemove = new();
  1002. foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
  1003. {
  1004. if (chunk.Surface.IsFullyTransparent())
  1005. {
  1006. toRemove.Add(pos);
  1007. chunk.Dispose();
  1008. }
  1009. }
  1010. foreach (var pos in toRemove)
  1011. {
  1012. committedChunks[ChunkResolution.Full].Remove(pos);
  1013. committedChunks[ChunkResolution.Half].Remove(pos);
  1014. committedChunks[ChunkResolution.Quarter].Remove(pos);
  1015. committedChunks[ChunkResolution.Eighth].Remove(pos);
  1016. }
  1017. }
  1018. /// <summary>
  1019. /// Gets existing committed chunk or creates a new one. Doesn't apply any operations to the chunk, returns it as it is.
  1020. /// </summary>
  1021. private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
  1022. {
  1023. // committed chunk of the same resolution exists
  1024. Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
  1025. if (targetChunk is not null)
  1026. return targetChunk;
  1027. // for full res chunks: nothing exists, create brand new chunk
  1028. if (resolution == ChunkResolution.Full)
  1029. {
  1030. var newChunk = Chunk.Create(resolution);
  1031. committedChunks[resolution][chunkPos] = newChunk;
  1032. return newChunk;
  1033. }
  1034. // for low res chunks: full res version exists
  1035. Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  1036. if (existingFullResChunk is not null)
  1037. {
  1038. var newChunk = Chunk.Create(resolution);
  1039. newChunk.Surface.DrawingSurface.Canvas.Save();
  1040. newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
  1041. newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0, SmoothReplacingPaint);
  1042. newChunk.Surface.DrawingSurface.Canvas.Restore();
  1043. committedChunks[resolution][chunkPos] = newChunk;
  1044. return newChunk;
  1045. }
  1046. // for low res chunks: full res version doesn't exist
  1047. {
  1048. GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
  1049. var newChunk = Chunk.Create(resolution);
  1050. committedChunks[resolution][chunkPos] = newChunk;
  1051. return newChunk;
  1052. }
  1053. }
  1054. /// <summary>
  1055. /// 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.
  1056. /// </summary>
  1057. private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
  1058. {
  1059. // latest chunk exists
  1060. Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
  1061. if (targetChunk is not null)
  1062. return targetChunk;
  1063. // committed chunk of the same resolution exists
  1064. var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
  1065. if (maybeCommittedAnyRes is not null)
  1066. {
  1067. Chunk newChunk = Chunk.Create(resolution);
  1068. if (blendMode == BlendMode.Src)
  1069. maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
  1070. else
  1071. newChunk.Surface.DrawingSurface.Canvas.Clear();
  1072. latestChunks[resolution][chunkPos] = newChunk;
  1073. return newChunk;
  1074. }
  1075. // committed chunk of full resolution exists
  1076. var maybeCommittedFullRes = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  1077. if (maybeCommittedFullRes is not null)
  1078. {
  1079. //create low res committed chunk
  1080. var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
  1081. //create latest based on it
  1082. Chunk newChunk = Chunk.Create(resolution);
  1083. committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
  1084. latestChunks[resolution][chunkPos] = newChunk;
  1085. return newChunk;
  1086. }
  1087. // no previous chunks exist
  1088. var newLatestChunk = Chunk.Create(resolution);
  1089. newLatestChunk.Surface.DrawingSurface.Canvas.Clear();
  1090. latestChunks[resolution][chunkPos] = newLatestChunk;
  1091. return newLatestChunk;
  1092. }
  1093. private void ThrowIfDisposed()
  1094. {
  1095. if (disposed)
  1096. throw new ObjectDisposedException(nameof(ChunkyImage));
  1097. }
  1098. public void Dispose()
  1099. {
  1100. lock (lockObject)
  1101. {
  1102. if (disposed)
  1103. return;
  1104. CancelChanges();
  1105. DisposeAll();
  1106. blendModePaint.Dispose();
  1107. GC.SuppressFinalize(this);
  1108. }
  1109. }
  1110. private void DisposeAll()
  1111. {
  1112. foreach (var (_, chunks) in committedChunks)
  1113. {
  1114. foreach (var (_, chunk) in chunks)
  1115. {
  1116. chunk.Dispose();
  1117. }
  1118. }
  1119. foreach (var (_, chunks) in latestChunks)
  1120. {
  1121. foreach (var (_, chunk) in chunks)
  1122. {
  1123. chunk.Dispose();
  1124. }
  1125. }
  1126. disposed = true;
  1127. }
  1128. }