diff --git a/documentation/brushfire.png b/documentation/brushfire.png index 540b68d..3e3383d 100644 Binary files a/documentation/brushfire.png and b/documentation/brushfire.png differ diff --git a/documentation/flow-field-path.png b/documentation/flow-field-path.png index 2b74368..17e7db5 100644 Binary files a/documentation/flow-field-path.png and b/documentation/flow-field-path.png differ diff --git a/documentation/flow-field.png b/documentation/flow-field.png index 38840d3..70d73e8 100644 Binary files a/documentation/flow-field.png and b/documentation/flow-field.png differ diff --git a/documentation/goal-based-pathfinder.png b/documentation/goal-based-pathfinder.png index c636a01..bf5f03a 100644 Binary files a/documentation/goal-based-pathfinder.png and b/documentation/goal-based-pathfinder.png differ diff --git a/documentation/jump-point-search-pathfinder.png b/documentation/jump-point-search-pathfinder.png index 5b5be09..6adcccd 100644 Binary files a/documentation/jump-point-search-pathfinder.png and b/documentation/jump-point-search-pathfinder.png differ diff --git a/test/NosCore.PathFinder.Tests/BrushFireTests.cs b/test/NosCore.PathFinder.Tests/BrushFireTests.cs index 3571823..1bb0350 100644 --- a/test/NosCore.PathFinder.Tests/BrushFireTests.cs +++ b/test/NosCore.PathFinder.Tests/BrushFireTests.cs @@ -8,11 +8,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.PathFinder.Brushfire; using NosCore.PathFinder.Heuristic; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -27,49 +22,36 @@ public void Test_BrushFire() { (short X, short Y) characterPosition = (6, 10); var brushFire = _map.LoadBrushFire(characterPosition, new OctileDistanceHeuristic()); - using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); - var listPixel = new List(); + var image = new MapCanvas(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var listPixel = new List(); TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, (0, 0), characterPosition); - var font = TestHelper.GetFont(); - image.Mutate(ctx => + for (short y = 0; y < _map.Height; y++) { - for (short y = 0; y < _map.Height; y++) + for (short x = 0; x < _map.Width; x++) { - for (short x = 0; x < _map.Width; x++) + if ((x, y) == characterPosition) { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - if ((x, y) != characterPosition) - { - if (brushFire[x, y] != null) - { - ctx.Fill(Color.White, rect); - var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); - var color = new Rgba32(0, 0, 255, alpha); - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, brushFire[x, y]?.ToString("N0") ?? "", Color.Black); - ctx.Fill(color, rect); - listPixel.Add(color); - } - else - { - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, "∞", Color.White); - } - } + continue; + } + + var centerX = x * TestHelper.Scale + TestHelper.Scale / 2f; + var centerY = y * TestHelper.Scale + TestHelper.Scale / 2f; + if (brushFire[x, y] != null) + { + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, Colors.White); + var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); + var color = new Rgba(0, 0, 255, alpha); + image.DrawTextCentered(centerX, centerY, brushFire[x, y]?.ToString("N0") ?? "", Colors.Black); + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + listPixel.Add(color); + } + else + { + image.DrawTextCentered(centerX, centerY, "∞", Colors.White); } } - }); + } TestHelper.VerifyFile("brushfire.png", image, listPixel, "Brushfire"); } diff --git a/test/NosCore.PathFinder.Tests/FlowFieldTests.cs b/test/NosCore.PathFinder.Tests/FlowFieldTests.cs index e4f286d..8329f78 100644 --- a/test/NosCore.PathFinder.Tests/FlowFieldTests.cs +++ b/test/NosCore.PathFinder.Tests/FlowFieldTests.cs @@ -10,11 +10,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.PathFinder.Brushfire; using NosCore.PathFinder.Heuristic; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -30,31 +25,24 @@ public void Test_FlowField() var brushFire = _map.LoadBrushFire(characterPosition, new OctileDistanceHeuristic()); var flowField = brushFire.GetFlowField(_map); - using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); - var listPixel = new List(); + var image = new MapCanvas(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var listPixel = new List(); TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, (0, 0), characterPosition); - image.Mutate(ctx => + for (short y = 0; y < _map.Height; y++) { - for (short y = 0; y < _map.Height; y++) + for (short x = 0; x < _map.Width; x++) { - for (short x = 0; x < _map.Width; x++) + if ((x, y) != characterPosition && brushFire[x, y] != null) { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - if ((x, y) != characterPosition) - { - if (brushFire[x, y] != null) - { - ctx.Fill(Color.White, rect); - var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); - var color = new Rgba32(0, 0, 255, alpha); - ctx.Fill(color, rect); - listPixel.Add(color); - } - } + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, Colors.White); + var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); + var color = new Rgba(0, 0, 255, alpha); + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + listPixel.Add(color); } } - }); + } for (short y = 0; y < _map.Height; y++) { @@ -65,7 +53,7 @@ public void Test_FlowField() var vector = flowField[x, y]; if (vector != null) { - TestHelper.DrawArrow(image, x, y, vector.Value.X, vector.Value.Y, TestHelper.Scale, Color.White.ToPixel()); + TestHelper.DrawArrow(image, x, y, vector.Value.X, vector.Value.Y, TestHelper.Scale, Colors.White); } } } @@ -85,32 +73,24 @@ public void Test_FlowField_MonsterPath() var path = TraceFlowFieldPath(flowField, monsterPosition, characterPosition); - using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); - var listPixel = new List(); + var image = new MapCanvas(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var listPixel = new List(); TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, monsterPosition, characterPosition); - var font = TestHelper.GetFont(); - image.Mutate(ctx => + for (short y = 0; y < _map.Height; y++) { - for (short y = 0; y < _map.Height; y++) + for (short x = 0; x < _map.Width; x++) { - for (short x = 0; x < _map.Width; x++) + if ((x, y) != characterPosition && (x, y) != monsterPosition && brushFire[x, y] != null) { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - if ((x, y) != characterPosition && (x, y) != monsterPosition) - { - if (brushFire[x, y] != null) - { - ctx.Fill(Color.White, rect); - var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); - var color = new Rgba32(0, 0, 255, alpha); - ctx.Fill(color, rect); - listPixel.Add(color); - } - } + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, Colors.White); + var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); + var color = new Rgba(0, 0, 255, alpha); + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + listPixel.Add(color); } } - }); + } for (short y = 0; y < _map.Height; y++) { @@ -121,34 +101,25 @@ public void Test_FlowField_MonsterPath() var vector = flowField[x, y]; if (vector != null) { - TestHelper.DrawArrow(image, x, y, vector.Value.X, vector.Value.Y, TestHelper.Scale, Color.White.ToPixel()); + TestHelper.DrawArrow(image, x, y, vector.Value.X, vector.Value.Y, TestHelper.Scale, Colors.White); } } } } var pathArray = path.ToArray(); - image.Mutate(ctx => + for (var i = 0; i < pathArray.Length; i++) { - for (var i = 0; i < pathArray.Length; i++) + var (x, y) = pathArray[i]; + if ((x, y) != monsterPosition && (x, y) != characterPosition) { - var (x, y) = pathArray[i]; - if ((x, y) != monsterPosition && (x, y) != characterPosition) - { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - var color = Color.LightPink; - ctx.Fill(color, rect); - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, i.ToString(), Color.Black); - listPixel.Add(color.ToPixel()); - } + var color = Colors.LightPink; + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + image.DrawTextCentered(x * TestHelper.Scale + TestHelper.Scale / 2f, + y * TestHelper.Scale + TestHelper.Scale / 2f, i.ToString(), Colors.Black); + listPixel.Add(color); } - }); + } TestHelper.VerifyFile("flow-field-path.png", image, listPixel, "Flow Field Path (Monster following vectors to Player)"); } diff --git a/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs b/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs index 0f47b9a..9322c74 100644 --- a/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs +++ b/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs @@ -12,11 +12,6 @@ using NosCore.PathFinder.Heuristic; using NosCore.PathFinder.Interfaces; using NosCore.PathFinder.Pathfinder; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -39,73 +34,51 @@ public GoalBasedPathfinderTests() [TestMethod] public void Test_GoalBasedPathfinder() { - using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var image = new MapCanvas(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); (short X, short Y) target = (15, 16); - var listPixel = new List(); + var listPixel = new List(); TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, target, _characterPosition); - var font = TestHelper.GetFont(); - image.Mutate(ctx => + for (short y = 0; y < _map.Height; y++) { - for (short y = 0; y < _map.Height; y++) + for (short x = 0; x < _map.Width; x++) { - for (short x = 0; x < _map.Width; x++) + if ((x, y) == target || (x, y) == _characterPosition) { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - if ((x, y) != target && (x, y) != _characterPosition) - { - if (_brushFire[x, y] != null) - { - ctx.Fill(Color.White, rect); - var alpha = (byte)((_brushFire[x, y] * 12 > 255 ? 255 : (_brushFire[x, y] ?? 0) * 12)); - var color = new Rgba32(0, 0, 255, alpha); - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, _brushFire[x, y]?.ToString("N0") ?? "", Color.Black); - ctx.Fill(color, rect); - listPixel.Add(color); - } - else - { - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, "∞", Color.White); - } - } + continue; } - } - }); + var centerX = x * TestHelper.Scale + TestHelper.Scale / 2f; + var centerY = y * TestHelper.Scale + TestHelper.Scale / 2f; + if (_brushFire[x, y] != null) + { + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, Colors.White); + var alpha = (byte)((_brushFire[x, y] * 12 > 255 ? 255 : (_brushFire[x, y] ?? 0) * 12)); + var color = new Rgba(0, 0, 255, alpha); + image.DrawTextCentered(centerX, centerY, _brushFire[x, y]?.ToString("N0") ?? "", Colors.Black); + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + listPixel.Add(color); + } + else + { + image.DrawTextCentered(centerX, centerY, "∞", Colors.White); + } + } + } var path = _goalPathfinder.FindPath(target, _characterPosition).ToList(); - image.Mutate(ctx => + foreach (var (x, y) in path) { - foreach (var (x, y) in path) + if ((x, y) != target && (x, y) != _characterPosition) { - if ((x, y) != target && (x, y) != _characterPosition) - { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - var color = Color.LightPink; - ctx.Fill(color, rect); - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, Array.IndexOf(path.ToArray(), (x, y)).ToString(), Color.Black); - listPixel.Add(color.ToPixel()); - } + var color = Colors.LightPink; + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + image.DrawTextCentered(x * TestHelper.Scale + TestHelper.Scale / 2f, + y * TestHelper.Scale + TestHelper.Scale / 2f, + Array.IndexOf(path.ToArray(), (x, y)).ToString(), Colors.Black); + listPixel.Add(color); } - }); + } TestHelper.VerifyFile("goal-based-pathfinder.png", image, listPixel, "Goal Based Pathfinder"); } diff --git a/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs b/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs index e6b28cd..087c2dc 100644 --- a/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs +++ b/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs @@ -10,11 +10,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.PathFinder.Heuristic; using NosCore.PathFinder.Pathfinder; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -35,40 +30,26 @@ public JumpPointSearchPathfinderTests() [TestMethod] public void Test_JumpPointSearchPathfinder() { - using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var image = new MapCanvas(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); (short X, short Y) target = (15, 16); - var listPixel = new List(); + var listPixel = new List(); TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, target, _characterPosition); - var font = TestHelper.GetFont(); var jumps = _jumpPointSearchPathfinder.GetJumpList(target, _characterPosition).ToList(); var path = _jumpPointSearchPathfinder.FindPath(target, _characterPosition).ToList(); - image.Mutate(ctx => + foreach (var (x, y) in path) { - foreach (var (x, y) in path) + if ((x, y) != target && (x, y) != _characterPosition) { - if ((x, y) != target && (x, y) != _characterPosition) - { - var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - var color = Color.LightPink; - if (jumps.Contains((x, y))) - { - color = Color.DeepPink; - } - - ctx.Fill(color, rect); - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, Array.IndexOf(path.ToArray(), (x, y)).ToString(), Color.Black); - listPixel.Add(color.ToPixel()); - } + var color = jumps.Contains((x, y)) ? Colors.DeepPink : Colors.LightPink; + image.FillRect(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale, color); + image.DrawTextCentered(x * TestHelper.Scale + TestHelper.Scale / 2f, + y * TestHelper.Scale + TestHelper.Scale / 2f, + Array.IndexOf(path.ToArray(), (x, y)).ToString(), Colors.Black); + listPixel.Add(color); } - }); + } TestHelper.VerifyFile("jump-point-search-pathfinder.png", image, listPixel, "Jump Point Search Pathfinder (break at walls)"); } diff --git a/test/NosCore.PathFinder.Tests/MapCanvas.cs b/test/NosCore.PathFinder.Tests/MapCanvas.cs new file mode 100644 index 0000000..88d3117 --- /dev/null +++ b/test/NosCore.PathFinder.Tests/MapCanvas.cs @@ -0,0 +1,223 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System; +using NetVips; + +namespace NosCore.PathFinder.Tests +{ + public readonly record struct Rgba(byte R, byte G, byte B, byte A) + { + public Rgba(byte r, byte g, byte b) : this(r, g, b, 255) + { + } + } + + public static class Colors + { + public static readonly Rgba Black = new(0, 0, 0); + public static readonly Rgba White = new(255, 255, 255); + public static readonly Rgba Blue = new(0, 0, 255); + public static readonly Rgba Green = new(0, 128, 0); + public static readonly Rgba DarkRed = new(139, 0, 0); + public static readonly Rgba LightPink = new(255, 182, 193); + public static readonly Rgba DeepPink = new(255, 20, 147); + } + + /// + /// A plain RGBA byte surface. Compositing is done here because the shapes are + /// axis-aligned and trivial; libvips is used for the two parts that genuinely need + /// a library, glyph rasterisation and PNG encoding. + /// + public sealed class MapCanvas + { + private const string FontSpec = "sans 16"; + + private static readonly Lazy FontAvailable = new(() => + { + try + { + using var probe = Image.Text("0", font: FontSpec, dpi: 72); + return true; + } + catch (VipsException) + { + return false; + } + }); + + private readonly byte[] _pixels; + + public MapCanvas(int width, int height) + { + Width = width; + Height = height; + _pixels = new byte[width * height * 4]; + } + + public int Width { get; } + public int Height { get; } + + public void FillRect(float x, float y, float width, float height, Rgba color) + { + var left = Math.Max(0, (int)MathF.Round(x)); + var top = Math.Max(0, (int)MathF.Round(y)); + var right = Math.Min(Width, (int)MathF.Round(x + width)); + var bottom = Math.Min(Height, (int)MathF.Round(y + height)); + + for (var py = top; py < bottom; py++) + { + for (var px = left; px < right; px++) + { + Blend(px, py, color, color.A); + } + } + } + + public void DrawLine(float x1, float y1, float x2, float y2, float thickness, Rgba color) + { + var radius = thickness / 2f; + var left = Math.Max(0, (int)MathF.Floor(Math.Min(x1, x2) - radius)); + var top = Math.Max(0, (int)MathF.Floor(Math.Min(y1, y2) - radius)); + var right = Math.Min(Width - 1, (int)MathF.Ceiling(Math.Max(x1, x2) + radius)); + var bottom = Math.Min(Height - 1, (int)MathF.Ceiling(Math.Max(y1, y2) + radius)); + + for (var py = top; py <= bottom; py++) + { + for (var px = left; px <= right; px++) + { + if (DistanceToSegment(px + 0.5f, py + 0.5f, x1, y1, x2, y2) <= radius) + { + Blend(px, py, color, color.A); + } + } + } + } + + public void FillPolygon(ReadOnlySpan<(float X, float Y)> points, Rgba color) + { + var minX = Width - 1; + var minY = Height - 1; + var maxX = 0; + var maxY = 0; + foreach (var (x, y) in points) + { + minX = Math.Min(minX, Math.Max(0, (int)MathF.Floor(x))); + minY = Math.Min(minY, Math.Max(0, (int)MathF.Floor(y))); + maxX = Math.Max(maxX, Math.Min(Width - 1, (int)MathF.Ceiling(x))); + maxY = Math.Max(maxY, Math.Min(Height - 1, (int)MathF.Ceiling(y))); + } + + for (var py = minY; py <= maxY; py++) + { + for (var px = minX; px <= maxX; px++) + { + if (Contains(points, px + 0.5f, py + 0.5f)) + { + Blend(px, py, color, color.A); + } + } + } + } + + public void DrawTextCentered(float centerX, float centerY, string text, Rgba color) + { + if (string.IsNullOrEmpty(text) || !FontAvailable.Value) + { + return; + } + + using var glyphs = Image.Text(text, font: FontSpec, dpi: 72); + var mask = glyphs.WriteToMemory(); + var originX = (int)MathF.Round(centerX - glyphs.Width / 2f); + var originY = (int)MathF.Round(centerY - glyphs.Height / 2f); + + for (var gy = 0; gy < glyphs.Height; gy++) + { + for (var gx = 0; gx < glyphs.Width; gx++) + { + var coverage = mask[gy * glyphs.Width + gx]; + var px = originX + gx; + var py = originY + gy; + if (coverage == 0 || px < 0 || py < 0 || px >= Width || py >= Height) + { + continue; + } + + Blend(px, py, color, (byte)(color.A * coverage / 255)); + } + } + } + + public void SaveAsPng(string path) + { + using var image = Image.NewFromMemory(_pixels, Width, Height, 4, Enums.BandFormat.Uchar) + .Copy(interpretation: Enums.Interpretation.Srgb); + image.WriteToFile(path); + } + + private void Blend(int x, int y, Rgba source, byte sourceAlpha) + { + var i = (y * Width + x) * 4; + if (sourceAlpha == 255) + { + _pixels[i] = source.R; + _pixels[i + 1] = source.G; + _pixels[i + 2] = source.B; + _pixels[i + 3] = 255; + return; + } + + var sa = sourceAlpha / 255f; + var da = _pixels[i + 3] / 255f; + var outA = sa + da * (1 - sa); + if (outA <= 0) + { + return; + } + + _pixels[i] = Composite(source.R, _pixels[i], sa, da, outA); + _pixels[i + 1] = Composite(source.G, _pixels[i + 1], sa, da, outA); + _pixels[i + 2] = Composite(source.B, _pixels[i + 2], sa, da, outA); + _pixels[i + 3] = (byte)MathF.Round(outA * 255); + } + + private static byte Composite(byte source, byte destination, float sa, float da, float outA) => + (byte)MathF.Round((source * sa + destination * da * (1 - sa)) / outA); + + private static float DistanceToSegment(float px, float py, float x1, float y1, float x2, float y2) + { + var dx = x2 - x1; + var dy = y2 - y1; + var lengthSquared = dx * dx + dy * dy; + if (lengthSquared <= float.Epsilon) + { + return MathF.Sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1)); + } + + var t = Math.Clamp(((px - x1) * dx + (py - y1) * dy) / lengthSquared, 0f, 1f); + var cx = x1 + t * dx - px; + var cy = y1 + t * dy - py; + return MathF.Sqrt(cx * cx + cy * cy); + } + + private static bool Contains(ReadOnlySpan<(float X, float Y)> points, float px, float py) + { + var inside = false; + for (int i = 0, j = points.Length - 1; i < points.Length; j = i++) + { + var (xi, yi) = points[i]; + var (xj, yj) = points[j]; + if (yi > py != yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) + { + inside = !inside; + } + } + + return inside; + } + } +} diff --git a/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj b/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj index efc380d..6f73b18 100644 --- a/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj +++ b/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj @@ -13,9 +13,10 @@ - - - + + + + diff --git a/test/NosCore.PathFinder.Tests/TestHelper.cs b/test/NosCore.PathFinder.Tests/TestHelper.cs index 6d8c103..4956be0 100644 --- a/test/NosCore.PathFinder.Tests/TestHelper.cs +++ b/test/NosCore.PathFinder.Tests/TestHelper.cs @@ -12,17 +12,12 @@ using System.Text; using ApprovalTests; using ApprovalTests.Writers; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { public static class TestHelper { - public static void VerifyFile(string linearPathfinderPng, Image image, List listPixel, string desc) + public static void VerifyFile(string linearPathfinderPng, MapCanvas image, List listPixel, string desc) { var filepath = Path.GetFullPath($"../../../../../documentation/{linearPathfinderPng}"); image.SaveAsPng(filepath); @@ -43,31 +38,6 @@ public static void VerifyFile(string linearPathfinderPng, Image image, L public static int Scale = 50; - private static Font? _font; - private static bool _fontLoaded; - public static Font? GetFont() - { - if (!_fontLoaded) - { - _fontLoaded = true; - var fontNames = new[] { "Arial", "DejaVu Sans", "Liberation Sans", "Noto Sans", "FreeSans" }; - foreach (var fontName in fontNames) - { - if (SystemFonts.TryGet(fontName, out var family)) - { - _font = family.CreateFont(16); - break; - } - } - - if (_font == null && SystemFonts.Collection.Families.Any()) - { - _font = SystemFonts.Collection.Families.First().CreateFont(16); - } - } - return _font; - } - public static TestMap SimpleMap = new TestMap(new[] { new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, @@ -91,53 +61,43 @@ public static void VerifyFile(string linearPathfinderPng, Image image, L new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} }); - public static void DrawMap(TestMap map, int scale, List listPixel, Image image, (short X, short Y) monster, (short X, short Y) character) + public static void DrawMap(TestMap map, int scale, List listPixel, MapCanvas image, (short X, short Y) monster, (short X, short Y) character) { - var font = GetFont(); - image.Mutate(ctx => + for (short y = 0; y < map.Height; y++) { - for (short y = 0; y < map.Height; y++) + for (short x = 0; x < map.Width; x++) { - for (short x = 0; x < map.Width; x++) + var color = Colors.Blue; + string? text = null; + if (!map.IsWalkable(x, y)) { - var rect = new RectangleF(x * scale, y * scale, scale, scale); - var color = Color.Blue; - string? text = null; - if (!map.IsWalkable(x, y)) - { - color = Color.Black; - } - - if (character == (x, y)) - { - text = "P"; - color = Color.Green; - } - - if (monster != default && monster == (x, y)) - { - text = "M"; - color = Color.DarkRed; - } - ctx.Fill(color, rect); - if (text != null && font != null) - { - var textOptions = new RichTextOptions(font) - { - Origin = new PointF(x * scale + scale / 2f, y * scale + scale / 2f), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - ctx.DrawText(textOptions, text, Color.Black); - } - - listPixel.Add(color.ToPixel()); + color = Colors.Black; } + + if (character == (x, y)) + { + text = "P"; + color = Colors.Green; + } + + if (monster != default && monster == (x, y)) + { + text = "M"; + color = Colors.DarkRed; + } + + image.FillRect(x * scale, y * scale, scale, scale, color); + if (text != null) + { + image.DrawTextCentered(x * scale + scale / 2f, y * scale + scale / 2f, text, Colors.Black); + } + + listPixel.Add(color); } - }); + } } - public static void DrawArrow(Image image, int cellX, int cellY, float dirX, float dirY, int scale, Rgba32 color) + public static void DrawArrow(MapCanvas image, int cellX, int cellY, float dirX, float dirY, int scale, Rgba color) { var centerX = cellX * scale + scale / 2f; var centerY = cellY * scale + scale / 2f; @@ -148,19 +108,15 @@ public static void DrawArrow(Image image, int cellX, int cellY, float di var endX = centerX + dirX * arrowLength; var endY = centerY + dirY * arrowLength; - image.Mutate(ctx => - { - ctx.DrawLine(color, 2f, new PointF(centerX, centerY), new PointF(endX, endY)); + image.DrawLine(centerX, centerY, endX, endY, 2f, color); - var angle = (float)Math.Atan2(dirY, dirX); - var head1X = endX - headLength * (float)Math.Cos(angle - 0.5f); - var head1Y = endY - headLength * (float)Math.Sin(angle - 0.5f); - var head2X = endX - headLength * (float)Math.Cos(angle + 0.5f); - var head2Y = endY - headLength * (float)Math.Sin(angle + 0.5f); + var angle = (float)Math.Atan2(dirY, dirX); + var head1X = endX - headLength * (float)Math.Cos(angle - 0.5f); + var head1Y = endY - headLength * (float)Math.Sin(angle - 0.5f); + var head2X = endX - headLength * (float)Math.Cos(angle + 0.5f); + var head2Y = endY - headLength * (float)Math.Sin(angle + 0.5f); - var headPoints = new PointF[] { new(endX, endY), new(head1X, head1Y), new(head2X, head2Y) }; - ctx.FillPolygon(color, headPoints); - }); + image.FillPolygon(new[] { (endX, endY), (head1X, head1Y), (head2X, head2Y) }, color); } } }