11public sealed class TilesheetRegion : IDisposable
18 private TilesheetRegionSlice?[,]? _tileCache;
19 private readonly Dictionary<(
int x,
int y),
CollisionAdjust> _frameCollisionAdjustments = [];
20 private readonly Dictionary<(
int x,
int y),
TileCollisionType> _frameCollisionTypes = [];
23 private bool _disposed;
25 private TilesheetRegion() { }
38 Tilesheet = tilesheet ??
throw new ArgumentNullException(nameof(tilesheet));
44 _tilePadding = tilePadding;
45 _regionMargin = regionMargin;
47 _collisionAdjust = collisionAdjust;
48 _collisionType = collisionType;
53 private Rectangle _area;
54 private Size _tileSize;
102 _tilePadding = value;
112 get => _regionMargin;
115 _regionMargin = value;
131 get => _collisionAdjust;
135 _collisionAdjust = value;
136 ApplyDefaultCollisionAdjustToCache();
144 _collisionAdjust.ApplyTo(
new Rectangle(Point.Empty, _tileSize));
152 get => _collisionType;
156 _collisionType = value;
163 public int Columns => _tileCache?.GetLength(0) ?? 0;
167 public int Rows => _tileCache?.GetLength(1) ?? 0;
187 if (_tileCache ==
null)
190 if (_tileCache ==
null ||
191 (uint)x >= (uint)_tileCache.GetLength(0) ||
192 (uint)y >= (uint)_tileCache.GetLength(1))
197 return _tileCache[x, y]?.Image;
210 if (_tileCache ==
null)
213 if (_tileCache ==
null ||
214 (uint)x >= (uint)_tileCache.GetLength(0) ||
215 (uint)y >= (uint)_tileCache.GetLength(1))
220 return _tileCache[x, y]?.Bitmap;
231 if (_tileCache ==
null)
234 var bitmaps =
new Dictionary<(int x, int y), SKBitmap>();
235 if (_tileCache ==
null)
238 for (
int y = 0; y < _tileCache.GetLength(1); y++)
240 for (
int x = 0; x < _tileCache.GetLength(0); x++)
242 if (_tileCache[x, y] is { } slice)
243 bitmaps[(x, y)] = slice.Bitmap;
258 if (_tileCache ==
null)
261 var images =
new Dictionary<(int x, int y), SKImage>();
262 if (_tileCache ==
null)
265 for (
int y = 0; y < _tileCache.GetLength(1); y++)
267 for (
int x = 0; x < _tileCache.GetLength(0); x++)
269 if (_tileCache[x, y] is { } slice)
270 images[(x, y)] = slice.Image;
287 if (!IsFrameCoordinateValid(x, y))
288 return _collisionAdjust;
290 if (_tileCache is not
null && _tileCache[x, y] is { } slice)
291 return slice.CollisionAdjust;
293 return GetStoredFrameCollisionAdjust(x, y);
315 if (_tileCache is
null)
318 if (!IsFrameCoordinateValid(x, y))
320 collisionAdjust =
default;
324 return _frameCollisionAdjustments.TryGetValue(
326 out collisionAdjust);
339 if (_tileCache is
null)
342 if (!IsFrameCoordinateValid(x, y))
344 throw new ArgumentOutOfRangeException(
346 $
"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
349 _frameCollisionAdjustments[(x, y)] = collisionAdjust;
351 if (_tileCache![x, y] is { } slice)
352 _tileCache[x, y] = slice.WithCollisionAdjust(collisionAdjust);
369 if (_tileCache is
null)
372 if (!IsFrameCoordinateValid(x, y))
374 throw new ArgumentOutOfRangeException(
376 $
"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
379 if (!_frameCollisionAdjustments.Remove((x, y)))
382 if (_tileCache![x, y] is { } slice)
383 _tileCache[x, y] = slice.WithCollisionAdjust(_collisionAdjust);
396 .
ApplyTo(
new Rectangle(Point.Empty, _tileSize));
408 if (!IsFrameCoordinateValid(x, y))
409 return _collisionType;
411 return _frameCollisionTypes.TryGetValue((x, y), out var collisionType)
430 if (_tileCache is
null)
433 if (!IsFrameCoordinateValid(x, y))
435 collisionType =
default;
439 return _frameCollisionTypes.TryGetValue((x, y), out collisionType);
452 if (_tileCache is
null)
455 if (!IsFrameCoordinateValid(x, y))
457 throw new ArgumentOutOfRangeException(
459 $
"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
462 _frameCollisionTypes[(x, y)] = collisionType;
476 if (_tileCache is
null)
479 if (!IsFrameCoordinateValid(x, y))
481 throw new ArgumentOutOfRangeException(
483 $
"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
486 return _frameCollisionTypes.Remove((x, y));
492 internal void BuildTileCache()
500 _tileSize.Width <= 0 ||
501 _tileSize.Height <= 0 ||
508 if (_tilePadding.Left < 0 || _tilePadding.Top < 0 ||
509 _tilePadding.Right < 0 || _tilePadding.Bottom < 0)
511 throw new InvalidOperationException(
"Tilesheet region tile padding cannot be negative.");
514 if (_regionMargin.Left < 0 || _regionMargin.Top < 0 ||
515 _regionMargin.Right < -_tilePadding.Right || _regionMargin.Bottom < -_tilePadding.Bottom)
517 throw new InvalidOperationException(
"Leading region margins cannot be negative; trailing margins may only cancel trailing tile padding.");
523 int xTiles = (_area.Width - _regionMargin.Left - _regionMargin.Right) /
525 int yTiles = (_area.Height - _regionMargin.Top - _regionMargin.Bottom) /
528 if (xTiles <= 0 || yTiles <= 0)
531 PruneFrameCollisionAdjustments(xTiles, yTiles);
532 PruneFrameCollisionTypes(xTiles, yTiles);
533 _tileCache =
new TilesheetRegionSlice?[xTiles, yTiles];
535 var regionArea =
Area;
538 for (
int y = 0; y < yTiles; y++)
540 for (
int x = 0; x < xTiles; x++)
542 var srcRect = GetTileBounds(x, y);
545 if (!regionArea.Contains(srcRect) ||
546 !bitmapBounds.Contains(srcRect.ToSKRectI()))
551 var slice = CreateSlice(
553 GetStoredFrameCollisionAdjust(x, y));
556 _tileCache[x, y] = slice.Value;
561 internal void ClearTileCache()
563 if (_tileCache ==
null)
566 for (
int y = 0; y < _tileCache.GetLength(1); y++)
568 for (
int x = 0; x < _tileCache.GetLength(0); x++)
570 _tileCache[x, y]?.Bitmap.Dispose();
571 _tileCache[x, y]?.Image.Dispose();
572 _tileCache[x, y] =
null;
579 internal Rectangle GetFrameSourceBounds(
int xTile,
int yTile)
583 if (_tileCache is
null)
586 return IsFrameCoordinateValid(xTile, yTile)
587 ? GetTileBounds(xTile, yTile)
591 private Rectangle GetTileBounds(
int xTile,
int yTile)
597 x + _tilePadding.Left,
598 y + _tilePadding.Top,
603 private TilesheetRegionSlice? CreateSlice(
608 var sliceInfo =
new SKImageInfo(
614 var bmp =
new SKBitmap(sliceInfo);
615 bmp.Erase(SKColors.Transparent);
617 if (!
Tilesheet.SkBitmap.ExtractSubset(bmp, srcRect.ToSKRectI()))
623 var img = SKImage.FromBitmap(bmp);
624 return new TilesheetRegionSlice(bmp, img, collisionAdjust);
628 _frameCollisionAdjustments.TryGetValue((x, y), out var collisionAdjust)
632 private bool IsFrameCoordinateValid(
int x,
int y) =>
633 _tileCache is not
null &&
634 (uint)x < (uint)_tileCache.GetLength(0) &&
635 (uint)y < (uint)_tileCache.GetLength(1);
637 private void ApplyDefaultCollisionAdjustToCache()
639 if (_tileCache is
null)
642 for (
int y = 0; y < _tileCache.GetLength(1); y++)
644 for (
int x = 0; x < _tileCache.GetLength(0); x++)
646 if (!_frameCollisionAdjustments.ContainsKey((x, y)) &&
647 _tileCache[x, y] is { } slice)
649 _tileCache[x, y] = slice.WithCollisionAdjust(_collisionAdjust);
655 private void PruneFrameCollisionAdjustments(
int columns,
int rows)
657 foreach (var key
in _frameCollisionAdjustments.Keys.ToArray())
659 if ((uint)key.x >= (uint)columns || (uint)key.y >= (uint)rows)
660 _frameCollisionAdjustments.Remove(key);
664 private void PruneFrameCollisionTypes(
int columns,
int rows)
666 foreach (var key
in _frameCollisionTypes.Keys.ToArray())
668 if ((uint)key.x >= (uint)columns || (uint)key.y >= (uint)rows)
669 _frameCollisionTypes.Remove(key);
673 private void ThrowIfDisposed()
676 throw new ObjectDisposedException(nameof(TilesheetRegion));
689 _frameCollisionAdjustments.Clear();
690 _frameCollisionTypes.Clear();
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.