ChunkyImage.cs 55 KB

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