mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-04-23 05:36:38 +00:00
geo fences - wip
This commit is contained in:
@@ -58,6 +58,12 @@ public class RouteProcessingService : BackgroundService
|
||||
private async Task ProcessPendingRoutesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var pendingRoutes = await GetRoutesWithPendingMapsAsync();
|
||||
|
||||
if (pendingRoutes.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Processing {Count} route(s) with pending maps: {RouteIds}",
|
||||
pendingRoutes.Count, string.Join(", ", pendingRoutes.Select(r => r.Id)));
|
||||
}
|
||||
|
||||
foreach (var route in pendingRoutes)
|
||||
{
|
||||
@@ -84,16 +90,37 @@ public class RouteProcessingService : BackgroundService
|
||||
private async Task ProcessRouteSequentiallyAsync(Guid routeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var route = await _routeRepository.GetByIdAsync(routeId);
|
||||
if (route == null || !route.RequestMaps || route.MapsReady)
|
||||
if (route == null)
|
||||
{
|
||||
_logger.LogWarning("Route {RouteId}: Route not found, skipping processing", routeId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!route.RequestMaps)
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId}: RequestMaps=false, skipping processing", routeId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (route.MapsReady)
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId}: MapsReady=true, skipping processing", routeId);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Route {RouteId}: Starting processing check - RequestMaps={RequestMaps}, MapsReady={MapsReady}",
|
||||
routeId, route.RequestMaps, route.MapsReady);
|
||||
|
||||
var routePointsList = (await _routeRepository.GetRoutePointsAsync(routeId)).ToList();
|
||||
var regionIdsList = (await _routeRepository.GetRegionIdsByRouteAsync(routeId)).ToList();
|
||||
var geofenceRegionIdsList = (await _routeRepository.GetGeofenceRegionIdsByRouteAsync(routeId)).ToList();
|
||||
|
||||
if (regionIdsList.Count == 0)
|
||||
var allRegionIds = regionIdsList.Union(geofenceRegionIdsList).ToList();
|
||||
|
||||
if (regionIdsList.Count == 0 && routePointsList.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId}: No route point regions linked yet. Will create regions for {PointCount} route points", routeId, routePointsList.Count);
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var regionService = scope.ServiceProvider.GetRequiredService<Common.Interfaces.IRegionService>();
|
||||
|
||||
@@ -106,6 +133,9 @@ public class RouteProcessingService : BackgroundService
|
||||
{
|
||||
var regionId = Guid.NewGuid();
|
||||
|
||||
_logger.LogInformation("RouteProcessingService - Creating region {RegionId} for route {RouteId} at point: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||
regionId, routeId, point.Latitude, point.Longitude);
|
||||
|
||||
await regionService.RequestRegionAsync(
|
||||
regionId,
|
||||
point.Latitude,
|
||||
@@ -124,7 +154,7 @@ public class RouteProcessingService : BackgroundService
|
||||
}
|
||||
|
||||
var regions = new List<DataAccess.Models.RegionEntity>();
|
||||
foreach (var regionId in regionIdsList)
|
||||
foreach (var regionId in allRegionIds)
|
||||
{
|
||||
var region = await _regionRepository.GetByIdAsync(regionId);
|
||||
if (region != null)
|
||||
@@ -137,16 +167,36 @@ public class RouteProcessingService : BackgroundService
|
||||
var failedRegions = regions.Where(r => r.Status == "failed").ToList();
|
||||
var processingRegions = regions.Where(r => r.Status == "queued" || r.Status == "processing").ToList();
|
||||
|
||||
var hasEnoughCompleted = completedRegions.Count >= routePointsList.Count;
|
||||
var completedRoutePointRegions = completedRegions.Where(r => !geofenceRegionIdsList.Contains(r.Id)).ToList();
|
||||
var completedGeofenceRegions = completedRegions.Where(r => geofenceRegionIdsList.Contains(r.Id)).ToList();
|
||||
|
||||
_logger.LogInformation("Route {RouteId}: Region counts - Total allRegionIds={AllCount}, regionIdsList={RoutePointCount}, geofenceRegionIdsList={GeofenceCount}",
|
||||
routeId, allRegionIds.Count, regionIdsList.Count, geofenceRegionIdsList.Count);
|
||||
_logger.LogInformation("Route {RouteId}: Status breakdown - Completed={Completed} (RoutePoint={CompletedRP}, Geofence={CompletedGF}), Failed={Failed}, Processing={Processing}",
|
||||
routeId, completedRegions.Count, completedRoutePointRegions.Count, completedGeofenceRegions.Count, failedRegions.Count, processingRegions.Count);
|
||||
|
||||
var hasRoutePointRegions = regionIdsList.Count > 0;
|
||||
var hasEnoughRoutePointRegions = !hasRoutePointRegions || completedRoutePointRegions.Count >= routePointsList.Count;
|
||||
var hasAllGeofenceRegions = geofenceRegionIdsList.Count == 0 || completedGeofenceRegions.Count >= geofenceRegionIdsList.Count;
|
||||
var hasEnoughCompleted = hasEnoughRoutePointRegions && hasAllGeofenceRegions;
|
||||
|
||||
_logger.LogInformation("Route {RouteId}: Condition checks - hasRoutePointRegions={HasRP}, hasEnoughRoutePointRegions={HasEnoughRP} (need {NeedRP}), hasAllGeofenceRegions={HasAllGF} (need {NeedGF}), hasEnoughCompleted={HasEnough}",
|
||||
routeId, hasRoutePointRegions, hasEnoughRoutePointRegions, routePointsList.Count, hasAllGeofenceRegions, geofenceRegionIdsList.Count, hasEnoughCompleted);
|
||||
|
||||
var activeRegions = completedRegions.Count + processingRegions.Count;
|
||||
var shouldRetryFailed = failedRegions.Count > 0 && !hasEnoughCompleted && activeRegions < routePointsList.Count;
|
||||
var shouldRetryFailed = failedRegions.Count > 0 && !hasEnoughCompleted && activeRegions < allRegionIds.Count;
|
||||
|
||||
if (hasEnoughCompleted)
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId}: Have {Completed} completed regions (required: {Required}). Generating final maps. Ignoring {Processing} processing and {Failed} failed regions.",
|
||||
routeId, completedRegions.Count, routePointsList.Count, processingRegions.Count, failedRegions.Count);
|
||||
_logger.LogInformation("Route {RouteId}: Have {RoutePointCompleted}/{RoutePointRequired} route point regions and {GeofenceCompleted}/{GeofenceRequired} geofence regions completed. Generating final maps. Ignoring {Processing} processing and {Failed} failed regions.",
|
||||
routeId, completedRoutePointRegions.Count, routePointsList.Count, completedGeofenceRegions.Count, geofenceRegionIdsList.Count, processingRegions.Count, failedRegions.Count);
|
||||
|
||||
await GenerateRouteMapsAsync(routeId, route, completedRegions.Take(routePointsList.Count).Select(r => r.Id), cancellationToken);
|
||||
var orderedRouteRegions = MatchRegionsToRoutePoints(routePointsList, completedRoutePointRegions, routeId);
|
||||
var routeRegionIds = orderedRouteRegions.Select(r => r.Id).ToList();
|
||||
var allRegionIdsForStitching = routeRegionIds.Concat(completedGeofenceRegions.Select(r => r.Id)).Distinct();
|
||||
var geofenceRegionIdsForBorders = completedGeofenceRegions.Select(r => r.Id).ToList();
|
||||
|
||||
await GenerateRouteMapsAsync(routeId, route, allRegionIdsForStitching, geofenceRegionIdsForBorders, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,16 +232,20 @@ public class RouteProcessingService : BackgroundService
|
||||
var anyProcessing = processingRegions.Count > 0;
|
||||
if (anyProcessing)
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId}: Progress - {Completed}/{Required} regions completed, {Processing} still processing, {Failed} failed (will retry if needed)",
|
||||
routeId, completedRegions.Count, routePointsList.Count, processingRegions.Count, failedRegions.Count);
|
||||
_logger.LogInformation("Route {RouteId}: Progress - {RoutePointCompleted}/{RoutePointRequired} route point regions, {GeofenceCompleted}/{GeofenceRequired} geofence regions completed, {Processing} still processing, {Failed} failed (will retry if needed)",
|
||||
routeId, completedRoutePointRegions.Count, routePointsList.Count, completedGeofenceRegions.Count, geofenceRegionIdsList.Count, processingRegions.Count, failedRegions.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Route {RouteId}: No processing regions, but not enough completed. This is an unexpected state - hasEnoughCompleted={HasEnough}, shouldRetryFailed={ShouldRetry}, anyProcessing={AnyProcessing}",
|
||||
routeId, hasEnoughCompleted, shouldRetryFailed, anyProcessing);
|
||||
}
|
||||
|
||||
private async Task GenerateRouteMapsAsync(
|
||||
Guid routeId,
|
||||
DataAccess.Models.RouteEntity route,
|
||||
IEnumerable<Guid> regionIds,
|
||||
List<Guid> geofenceRegionIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -208,21 +262,42 @@ public class RouteProcessingService : BackgroundService
|
||||
var region = await _regionRepository.GetByIdAsync(regionId);
|
||||
if (region == null || string.IsNullOrEmpty(region.CsvFilePath) || !File.Exists(region.CsvFilePath))
|
||||
{
|
||||
_logger.LogWarning("Region {RegionId} CSV not found for route {RouteId}", regionId, routeId);
|
||||
_logger.LogWarning("Route {RouteId}: Region {RegionId} CSV not found", routeId, regionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var isGeofence = geofenceRegionIds.Contains(regionId);
|
||||
_logger.LogInformation("Route {RouteId}: Processing region {RegionId} ({Type}) - Lat={Lat}, Lon={Lon}, Size={Size}m, CSV={CsvPath}",
|
||||
routeId, regionId, isGeofence ? "GEOFENCE" : "RoutePoint",
|
||||
region.Latitude, region.Longitude, region.SizeMeters, region.CsvFilePath);
|
||||
|
||||
var csvLines = await File.ReadAllLinesAsync(region.CsvFilePath, cancellationToken);
|
||||
|
||||
var lineNumber = 0;
|
||||
foreach (var line in csvLines.Skip(1))
|
||||
{
|
||||
lineNumber++;
|
||||
var parts = line.Split(',');
|
||||
if (parts.Length < 3) continue;
|
||||
|
||||
if (!double.TryParse(parts[0], out var lat)) continue;
|
||||
if (!double.TryParse(parts[1], out var lon)) continue;
|
||||
if (!double.TryParse(parts[0], out var lat))
|
||||
{
|
||||
_logger.LogWarning("Route {RouteId} - Failed to parse latitude from CSV line {LineNumber}: {Line}", routeId, lineNumber, line);
|
||||
continue;
|
||||
}
|
||||
if (!double.TryParse(parts[1], out var lon))
|
||||
{
|
||||
_logger.LogWarning("Route {RouteId} - Failed to parse longitude from CSV line {LineNumber}: {Line}", routeId, lineNumber, line);
|
||||
continue;
|
||||
}
|
||||
var filePath = parts[2];
|
||||
|
||||
if (lineNumber <= 3)
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId} - Reading tile from region {RegionId} CSV: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||
routeId, regionId, lat, lon);
|
||||
}
|
||||
|
||||
totalTilesFromRegions++;
|
||||
var key = $"{lat:F6}_{lon:F6}";
|
||||
|
||||
@@ -251,8 +326,54 @@ public class RouteProcessingService : BackgroundService
|
||||
string? stitchedImagePath = null;
|
||||
if (route.RequestMaps)
|
||||
{
|
||||
var geofenceTileBounds = new List<(Guid RegionId, int MinX, int MinY, int MaxX, int MaxY)>();
|
||||
|
||||
foreach (var geofenceId in geofenceRegionIds)
|
||||
{
|
||||
var region = await _regionRepository.GetByIdAsync(geofenceId);
|
||||
if (region != null && !string.IsNullOrEmpty(region.CsvFilePath) && File.Exists(region.CsvFilePath))
|
||||
{
|
||||
_logger.LogInformation("Route {RouteId}: Loading geofence region {RegionId} tile bounds",
|
||||
routeId, region.Id);
|
||||
|
||||
var csvLines = await File.ReadAllLinesAsync(region.CsvFilePath, cancellationToken);
|
||||
int? minX = null, minY = null, maxX = null, maxY = null;
|
||||
|
||||
foreach (var line in csvLines.Skip(1))
|
||||
{
|
||||
var parts = line.Split(',');
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
if (double.TryParse(parts[0], out var lat) && double.TryParse(parts[1], out var lon))
|
||||
{
|
||||
var tile = GeoUtils.WorldToTilePos(new Common.DTO.GeoPoint { Lat = lat, Lon = lon }, route.ZoomLevel);
|
||||
minX = minX == null ? tile.x : Math.Min(minX.Value, tile.x);
|
||||
minY = minY == null ? tile.y : Math.Min(minY.Value, tile.y);
|
||||
maxX = maxX == null ? tile.x : Math.Max(maxX.Value, tile.x);
|
||||
maxY = maxY == null ? tile.y : Math.Max(maxY.Value, tile.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minX.HasValue && minY.HasValue && maxX.HasValue && maxY.HasValue)
|
||||
{
|
||||
geofenceTileBounds.Add((region.Id, minX.Value, minY.Value, maxX.Value, maxY.Value));
|
||||
_logger.LogInformation("Route {RouteId}: Geofence {RegionId} tile bounds: X=[{MinX}..{MaxX}], Y=[{MinY}..{MaxY}]",
|
||||
routeId, region.Id, minX.Value, maxX.Value, minY.Value, maxY.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Route {RouteId}: Geofence region {RegionId} CSV not found",
|
||||
routeId, geofenceId);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Route {RouteId}: Starting stitching with {GeofenceCount} geofence regions",
|
||||
routeId, geofenceTileBounds.Count);
|
||||
|
||||
stitchedImagePath = Path.Combine(readyDir, $"route_{routeId}_stitched.jpg");
|
||||
await StitchRouteTilesAsync(allTiles.Values.ToList(), stitchedImagePath, route.ZoomLevel, cancellationToken);
|
||||
await StitchRouteTilesAsync(allTiles.Values.ToList(), stitchedImagePath, route.ZoomLevel, geofenceTileBounds, cancellationToken);
|
||||
}
|
||||
|
||||
var summaryPath = Path.Combine(readyDir, $"route_{routeId}_summary.txt");
|
||||
@@ -339,6 +460,7 @@ public class RouteProcessingService : BackgroundService
|
||||
List<TileInfo> tiles,
|
||||
string outputPath,
|
||||
int zoomLevel,
|
||||
List<(Guid RegionId, int MinX, int MinY, int MaxX, int MaxY)> geofenceTileBounds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (tiles.Count == 0)
|
||||
@@ -437,6 +559,48 @@ public class RouteProcessingService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
if (geofenceTileBounds.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Drawing {Count} geofence borders on image {Width}x{Height} (grid: minX={MinX}, minY={MinY})",
|
||||
geofenceTileBounds.Count, imageWidth, imageHeight, minX, minY);
|
||||
|
||||
foreach (var (regionId, geoMinX, geoMinY, geoMaxX, geoMaxY) in geofenceTileBounds)
|
||||
{
|
||||
_logger.LogInformation("Geofence {RegionId}: Tile range - X=[{MinX}..{MaxX}], Y=[{MinY}..{MaxY}]",
|
||||
regionId, geoMinX, geoMaxX, geoMinY, geoMaxY);
|
||||
|
||||
var x1 = (geoMinX - minX) * tileSizePixels;
|
||||
var y1 = (geoMinY - minY) * tileSizePixels;
|
||||
var x2 = (geoMaxX - minX + 1) * tileSizePixels - 1;
|
||||
var y2 = (geoMaxY - minY + 1) * tileSizePixels - 1;
|
||||
|
||||
_logger.LogInformation("Geofence {RegionId}: Pixel coords before clipping - ({X1},{Y1}) to ({X2},{Y2})",
|
||||
regionId, x1, y1, x2, y2);
|
||||
|
||||
x1 = Math.Max(0, Math.Min(x1, imageWidth - 1));
|
||||
y1 = Math.Max(0, Math.Min(y1, imageHeight - 1));
|
||||
x2 = Math.Max(0, Math.Min(x2, imageWidth - 1));
|
||||
y2 = Math.Max(0, Math.Min(y2, imageHeight - 1));
|
||||
|
||||
if (x1 >= 0 && y1 >= 0 && x2 < imageWidth && y2 < imageHeight && x2 > x1 && y2 > y1)
|
||||
{
|
||||
_logger.LogInformation("Geofence {RegionId}: Drawing border at pixel coords ({X1},{Y1}) to ({X2},{Y2})",
|
||||
regionId, x1, y1, x2, y2);
|
||||
|
||||
DrawRectangleBorder(stitchedImage, x1, y1, x2, y2, new Rgb24(255, 255, 0));
|
||||
|
||||
_logger.LogInformation("Successfully drew geofence border for region {RegionId}", regionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Geofence {RegionId}: Border out of bounds or invalid - ({X1},{Y1}) to ({X2},{Y2}), image size: {Width}x{Height}",
|
||||
regionId, x1, y1, x2, y2, imageWidth, imageHeight);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Completed drawing all geofence borders, now saving image...");
|
||||
}
|
||||
|
||||
await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);
|
||||
|
||||
var totalPossibleTiles = gridWidth * gridHeight;
|
||||
@@ -450,6 +614,54 @@ public class RouteProcessingService : BackgroundService
|
||||
totalPossibleTiles, gridWidth, gridHeight);
|
||||
}
|
||||
|
||||
private List<DataAccess.Models.RegionEntity> MatchRegionsToRoutePoints(
|
||||
List<DataAccess.Models.RoutePointEntity> routePoints,
|
||||
List<DataAccess.Models.RegionEntity> regions,
|
||||
Guid routeId)
|
||||
{
|
||||
var orderedRegions = new List<DataAccess.Models.RegionEntity>();
|
||||
var availableRegions = new List<DataAccess.Models.RegionEntity>(regions);
|
||||
|
||||
foreach (var point in routePoints)
|
||||
{
|
||||
var matchedRegion = availableRegions
|
||||
.OrderBy(r => CalculateDistance(point.Latitude, point.Longitude, r.Latitude, r.Longitude))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (matchedRegion != null)
|
||||
{
|
||||
orderedRegions.Add(matchedRegion);
|
||||
availableRegions.Remove(matchedRegion);
|
||||
|
||||
var distance = CalculateDistance(point.Latitude, point.Longitude, matchedRegion.Latitude, matchedRegion.Longitude);
|
||||
_logger.LogInformation("Route {RouteId}: Matched route point Seq={Seq} ({Lat:F6},{Lon:F6}) to region {RegionId} ({RegLat:F6},{RegLon:F6}), distance={Distance:F2}m",
|
||||
routeId, point.SequenceNumber, point.Latitude, point.Longitude,
|
||||
matchedRegion.Id, matchedRegion.Latitude, matchedRegion.Longitude, distance);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Route {RouteId}: No region found for route point Seq={Seq} ({Lat:F6},{Lon:F6})",
|
||||
routeId, point.SequenceNumber, point.Latitude, point.Longitude);
|
||||
}
|
||||
}
|
||||
|
||||
return orderedRegions;
|
||||
}
|
||||
|
||||
private static double CalculateDistance(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
const double earthRadiusMeters = 6371000;
|
||||
var dLat = (lat2 - lat1) * Math.PI / 180.0;
|
||||
var dLon = (lon2 - lon1) * Math.PI / 180.0;
|
||||
|
||||
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||
Math.Cos(lat1 * Math.PI / 180.0) * Math.Cos(lat2 * Math.PI / 180.0) *
|
||||
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||
|
||||
return earthRadiusMeters * c;
|
||||
}
|
||||
|
||||
private static (int TileX, int TileY) ExtractTileCoordinatesFromFilename(string filePath)
|
||||
{
|
||||
try
|
||||
@@ -471,6 +683,49 @@ public class RouteProcessingService : BackgroundService
|
||||
|
||||
return (-1, -1);
|
||||
}
|
||||
|
||||
private static (double NorthLat, double SouthLat, double WestLon, double EastLon) CalculateGeofenceCorners(
|
||||
double centerLat,
|
||||
double centerLon,
|
||||
double halfSizeMeters)
|
||||
{
|
||||
var center = new Common.DTO.GeoPoint { Lat = centerLat, Lon = centerLon };
|
||||
|
||||
var north = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 0 });
|
||||
var south = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 180 });
|
||||
var east = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 90 });
|
||||
var west = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 270 });
|
||||
|
||||
return (north.Lat, south.Lat, west.Lon, east.Lon);
|
||||
}
|
||||
|
||||
private static void DrawRectangleBorder(Image<Rgb24> image, int x1, int y1, int x2, int y2, Rgb24 color)
|
||||
{
|
||||
const int thickness = 5;
|
||||
|
||||
for (int t = 0; t < thickness; t++)
|
||||
{
|
||||
for (int x = x1; x <= x2; x++)
|
||||
{
|
||||
int topY = y1 + t;
|
||||
int bottomY = y2 - t;
|
||||
if (x >= 0 && x < image.Width && topY >= 0 && topY < image.Height)
|
||||
image[x, topY] = color;
|
||||
if (x >= 0 && x < image.Width && bottomY >= 0 && bottomY < image.Height)
|
||||
image[x, bottomY] = color;
|
||||
}
|
||||
|
||||
for (int y = y1; y <= y2; y++)
|
||||
{
|
||||
int leftX = x1 + t;
|
||||
int rightX = x2 - t;
|
||||
if (leftX >= 0 && leftX < image.Width && y >= 0 && y < image.Height)
|
||||
image[leftX, y] = color;
|
||||
if (rightX >= 0 && rightX < image.Width && y >= 0 && y < image.Height)
|
||||
image[rightX, y] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TileInfo
|
||||
|
||||
Reference in New Issue
Block a user