From a43df23e896a2ae735b6e8729e8262b935fbd742 Mon Sep 17 00:00:00 2001 From: mcmuzzle Date: Tue, 11 Mar 2025 23:37:12 +0100 Subject: [PATCH] structure du projet, ajout de la lib hexagonal --- .gitignore | 45 + Giants.sln | 50 ++ README.adoc | 6 + README.md | 2 - .../Giants.Application.csproj | 14 + .../Src/GiantApplication.cs | 18 + Src/Giants.Core/Giants.Core.csproj | 9 + Src/Giants.Core/Src/Entities/Match.cs | 23 + .../Src/Interfaces/IHexagonalGrid.cs | 9 + .../Src/ValuesObjects/BoardLayout.cs | 9 + .../Src/ValuesObjects/TileInfos.cs | 8 + .../Src/ValuesObjects/TilePosition.cs | 5 + .../Giants.Infrastructure.csproj | 14 + .../Src/HexagonalGridImpl.cs | 16 + Src/HexagonalLib/README.md | 127 +++ Src/HexagonalLib/img/hex-around.png | Bin 0 -> 45298 bytes Src/HexagonalLib/img/hex-corners-order.png | Bin 0 -> 10462 bytes Src/HexagonalLib/img/hex-neighbors-order.png | Bin 0 -> 12766 bytes Src/HexagonalLib/img/hex-rings.png | Bin 0 -> 40573 bytes Src/HexagonalLib/src/.editorconfig | 70 ++ Src/HexagonalLib/src/.gitignore | 4 + .../HexagonalGridTests.cs | 135 +++ .../HexagonalLib.Net.Tests.csproj | 26 + .../HexagonalMathTests.cs | 25 + .../HexagonalLib.Net/HexagonalLib.Net.csproj | 23 + .../HexagonalGridTests.cs | 57 ++ .../HexagonalLib.Unity.Tests.csproj | 78 ++ .../Properties/AssemblyInfo.cs | 35 + .../HexagonalLib.Unity.Tests/packages.config | 5 + .../HexagonalLib.Unity/Coordinates/Axial.cs | 7 + .../HexagonalLib.Unity/Coordinates/Cubic.cs | 6 + .../HexagonalLib.Unity/Coordinates/Offset.cs | 6 + .../HexagonalLib.Unity/HexagonalGrid.cs | 171 ++++ .../HexagonalLib.Unity.csproj | 66 ++ .../HexagonalLib.Unity/HexagonalMath.cs | 23 + .../Properties/AssemblyInfo.cs | 35 + .../Utility/VectorUtility.cs | 27 + .../HexagonalLib.Unity/packages.config | 4 + Src/HexagonalLib/src/HexagonalLib.sln | 50 ++ .../src/HexagonalLib/Coordinates/Axial.cs | 86 ++ .../src/HexagonalLib/Coordinates/Cubic.cs | 148 ++++ .../src/HexagonalLib/Coordinates/Offset.cs | 113 +++ .../src/HexagonalLib/HexagonalException.cs | 72 ++ .../src/HexagonalLib/HexagonalGrid.cs | 812 ++++++++++++++++++ .../src/HexagonalLib/HexagonalGridMesh.cs | 189 ++++ .../src/HexagonalLib/HexagonalGridType.cs | 28 + .../src/HexagonalLib/HexagonalLib.projitems | 21 + .../src/HexagonalLib/HexagonalLib.shproj | 18 + .../src/HexagonalLib/HexagonalMath.cs | 47 + Src/HexagonalLib/src/nuget.config | 5 + Tests/Giants.Core.Tests/ApplicationTests.cs | 12 + .../Giants.Core.Tests.csproj | 26 + 52 files changed, 2783 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Giants.sln create mode 100644 README.adoc delete mode 100644 README.md create mode 100644 Src/Giants.Application/Giants.Application.csproj create mode 100644 Src/Giants.Application/Src/GiantApplication.cs create mode 100644 Src/Giants.Core/Giants.Core.csproj create mode 100644 Src/Giants.Core/Src/Entities/Match.cs create mode 100644 Src/Giants.Core/Src/Interfaces/IHexagonalGrid.cs create mode 100644 Src/Giants.Core/Src/ValuesObjects/BoardLayout.cs create mode 100644 Src/Giants.Core/Src/ValuesObjects/TileInfos.cs create mode 100644 Src/Giants.Core/Src/ValuesObjects/TilePosition.cs create mode 100644 Src/Giants.Infrastructure/Giants.Infrastructure.csproj create mode 100644 Src/Giants.Infrastructure/Src/HexagonalGridImpl.cs create mode 100644 Src/HexagonalLib/README.md create mode 100644 Src/HexagonalLib/img/hex-around.png create mode 100644 Src/HexagonalLib/img/hex-corners-order.png create mode 100644 Src/HexagonalLib/img/hex-neighbors-order.png create mode 100644 Src/HexagonalLib/img/hex-rings.png create mode 100644 Src/HexagonalLib/src/.editorconfig create mode 100644 Src/HexagonalLib/src/.gitignore create mode 100644 Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalGridTests.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalLib.Net.Tests.csproj create mode 100644 Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalMathTests.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net/HexagonalLib.Net.csproj create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalGridTests.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalLib.Unity.Tests.csproj create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/Properties/AssemblyInfo.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/packages.config create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Axial.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Cubic.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Offset.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalGrid.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalLib.Unity.csproj create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalMath.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Properties/AssemblyInfo.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Utility/VectorUtility.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/packages.config create mode 100644 Src/HexagonalLib/src/HexagonalLib.sln create mode 100644 Src/HexagonalLib/src/HexagonalLib/Coordinates/Axial.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/Coordinates/Cubic.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/Coordinates/Offset.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalException.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalGrid.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalGridMesh.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalGridType.cs create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalLib.projitems create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalLib.shproj create mode 100644 Src/HexagonalLib/src/HexagonalLib/HexagonalMath.cs create mode 100644 Src/HexagonalLib/src/nuget.config create mode 100644 Tests/Giants.Core.Tests/ApplicationTests.cs create mode 100644 Tests/Giants.Core.Tests/Giants.Core.Tests.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fb328c --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode/* +!.vscode/settings.json + +# Rider +.idea/ + +# Visual Studio +.vs/ + +# Fleet +.fleet/ + +# Code Rush +.cr/ + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo].bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn \ No newline at end of file diff --git a/Giants.sln b/Giants.sln new file mode 100644 index 0000000..2c9bfe6 --- /dev/null +++ b/Giants.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{824D17BD-3CFE-498A-967D-3F42DF5EE92D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Giants.Core", "Src\Giants.Core\Giants.Core.csproj", "{A7B8B752-2AE9-4FB2-8F51-733F96C4CFD0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F34871C1-07BB-4012-A310-3FFDD6DC7DBA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Giants.Core.Tests", "Tests\Giants.Core.Tests\Giants.Core.Tests.csproj", "{30032CBC-0B7B-4BF3-9682-C506460F0FE6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Giants.Infrastructure", "Src\Giants.Infrastructure\Giants.Infrastructure.csproj", "{E6B2C76F-9CE2-4ADA-97F9-CEE24D48B65F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Giants.Application", "Src\Giants.Application\Giants.Application.csproj", "{1EF04517-2D31-4130-A687-7954A5431312}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A7B8B752-2AE9-4FB2-8F51-733F96C4CFD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7B8B752-2AE9-4FB2-8F51-733F96C4CFD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7B8B752-2AE9-4FB2-8F51-733F96C4CFD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7B8B752-2AE9-4FB2-8F51-733F96C4CFD0}.Release|Any CPU.Build.0 = Release|Any CPU + {30032CBC-0B7B-4BF3-9682-C506460F0FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30032CBC-0B7B-4BF3-9682-C506460F0FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30032CBC-0B7B-4BF3-9682-C506460F0FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30032CBC-0B7B-4BF3-9682-C506460F0FE6}.Release|Any CPU.Build.0 = Release|Any CPU + {E6B2C76F-9CE2-4ADA-97F9-CEE24D48B65F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6B2C76F-9CE2-4ADA-97F9-CEE24D48B65F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6B2C76F-9CE2-4ADA-97F9-CEE24D48B65F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6B2C76F-9CE2-4ADA-97F9-CEE24D48B65F}.Release|Any CPU.Build.0 = Release|Any CPU + {1EF04517-2D31-4130-A687-7954A5431312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EF04517-2D31-4130-A687-7954A5431312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EF04517-2D31-4130-A687-7954A5431312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EF04517-2D31-4130-A687-7954A5431312}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A7B8B752-2AE9-4FB2-8F51-733F96C4CFD0} = {824D17BD-3CFE-498A-967D-3F42DF5EE92D} + {30032CBC-0B7B-4BF3-9682-C506460F0FE6} = {F34871C1-07BB-4012-A310-3FFDD6DC7DBA} + {E6B2C76F-9CE2-4ADA-97F9-CEE24D48B65F} = {824D17BD-3CFE-498A-967D-3F42DF5EE92D} + {1EF04517-2D31-4130-A687-7954A5431312} = {824D17BD-3CFE-498A-967D-3F42DF5EE92D} + EndGlobalSection +EndGlobal diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..6645404 --- /dev/null +++ b/README.adoc @@ -0,0 +1,6 @@ +# Giants + +Projet d'adaptation du jeux de société Giants par matagot + +## Sources +- Lib de map hexagonal : HexagonalLib implementation for .NET(https://github.com/imurashka/HexagonalLib?tab=readme-ov-file) \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index f752b57..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Giants - diff --git a/Src/Giants.Application/Giants.Application.csproj b/Src/Giants.Application/Giants.Application.csproj new file mode 100644 index 0000000..e93fa9f --- /dev/null +++ b/Src/Giants.Application/Giants.Application.csproj @@ -0,0 +1,14 @@ + + + + + + + + + net8.0 + enable + enable + + + diff --git a/Src/Giants.Application/Src/GiantApplication.cs b/Src/Giants.Application/Src/GiantApplication.cs new file mode 100644 index 0000000..21bb61b --- /dev/null +++ b/Src/Giants.Application/Src/GiantApplication.cs @@ -0,0 +1,18 @@ +namespace Giants.Application; + +using Giants.Core.Interfaces; +using Giants.Infrastructure; + +/// +/// Une application Giants permettant l'instanciations des services, utilisé pour une console +/// ou autre archi sans injections de dependances +/// /// +public class GiantApplication +{ + + /// Constructeur de base + public GiantApplication() + { + IHexagonalGrid grid = new HexagonalGridImpl(); + } +} \ No newline at end of file diff --git a/Src/Giants.Core/Giants.Core.csproj b/Src/Giants.Core/Giants.Core.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Src/Giants.Core/Giants.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Src/Giants.Core/Src/Entities/Match.cs b/Src/Giants.Core/Src/Entities/Match.cs new file mode 100644 index 0000000..308797c --- /dev/null +++ b/Src/Giants.Core/Src/Entities/Match.cs @@ -0,0 +1,23 @@ +using Giants.Core.Interfaces; + +namespace Giants.Core; + +/// +/// Il s'agit d'une instance d'une partie de Giants, c'est la racine de toutes les entitées de l'instance +/// /// +public class Match +{ + #region données statiques + IHexagonalGrid _grid; + #endregion + + #region données dynamiques + #endregion + + /// Construction de base + /// La grid du jeu + public Match(IHexagonalGrid grid) + { + _grid = grid; + } +} \ No newline at end of file diff --git a/Src/Giants.Core/Src/Interfaces/IHexagonalGrid.cs b/Src/Giants.Core/Src/Interfaces/IHexagonalGrid.cs new file mode 100644 index 0000000..037403c --- /dev/null +++ b/Src/Giants.Core/Src/Interfaces/IHexagonalGrid.cs @@ -0,0 +1,9 @@ +namespace Giants.Core.Interfaces; + +/// +/// represente un outils qui permet de gérer une grille hexagonal +/// +public interface IHexagonalGrid +{ + +} \ No newline at end of file diff --git a/Src/Giants.Core/Src/ValuesObjects/BoardLayout.cs b/Src/Giants.Core/Src/ValuesObjects/BoardLayout.cs new file mode 100644 index 0000000..b8960bd --- /dev/null +++ b/Src/Giants.Core/Src/ValuesObjects/BoardLayout.cs @@ -0,0 +1,9 @@ +namespace Giants.Core; + +/// +/// Represente l'organisation d'un plateau, il ne contient aucune informations liées a une partie mais il permet +/// d'effectuer des recherches de chemins ou de type de tuiles +/// +public class BoardLayout +{ +} diff --git a/Src/Giants.Core/Src/ValuesObjects/TileInfos.cs b/Src/Giants.Core/Src/ValuesObjects/TileInfos.cs new file mode 100644 index 0000000..50ce365 --- /dev/null +++ b/Src/Giants.Core/Src/ValuesObjects/TileInfos.cs @@ -0,0 +1,8 @@ +namespace Giants.Core; + +/// +/// Represente les information statiques d'une case. Ses voisins, son type, aucune informations qui peut changer pendant une partie +/// +public class TileInfos +{ +} \ No newline at end of file diff --git a/Src/Giants.Core/Src/ValuesObjects/TilePosition.cs b/Src/Giants.Core/Src/ValuesObjects/TilePosition.cs new file mode 100644 index 0000000..cef932d --- /dev/null +++ b/Src/Giants.Core/Src/ValuesObjects/TilePosition.cs @@ -0,0 +1,5 @@ +namespace Giants.Core; + +public class TilePosition +{ +} \ No newline at end of file diff --git a/Src/Giants.Infrastructure/Giants.Infrastructure.csproj b/Src/Giants.Infrastructure/Giants.Infrastructure.csproj new file mode 100644 index 0000000..ec2a73a --- /dev/null +++ b/Src/Giants.Infrastructure/Giants.Infrastructure.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Src/Giants.Infrastructure/Src/HexagonalGridImpl.cs b/Src/Giants.Infrastructure/Src/HexagonalGridImpl.cs new file mode 100644 index 0000000..a270b71 --- /dev/null +++ b/Src/Giants.Infrastructure/Src/HexagonalGridImpl.cs @@ -0,0 +1,16 @@ + +namespace Giants.Infrastructure; + +using Giants.Core.Interfaces; + +using HexagonalLib; + +public class HexagonalGridImpl : IHexagonalGrid +{ + private readonly HexagonalGrid _grid; + + public HexagonalGridImpl() + { + _grid = new HexagonalGrid(HexagonalGridType.PointyEven, 1.0f); + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/README.md b/Src/HexagonalLib/README.md new file mode 100644 index 0000000..69bf62a --- /dev/null +++ b/Src/HexagonalLib/README.md @@ -0,0 +1,127 @@ +# HexagonalLib implementation for .NET + +_I was highly inspired by this article: [https://www.redblobgames.com/grids/hexagons/](https://www.redblobgames.com/grids/hexagons/) and this repo is mostly just C# implementation of it. Recommended for reading. Here I will describe some technical details regarding implementation._ + +## Hexagonal grid + +To use hexagonal grids you need to create [HexagonalGrid](src/HexagonalLib/HexagonalGrid.cs) and initialize it with grid type and inscribed radius of hex. + +```C# +var grid = new HexagonalGrid(HexagonalGridType.PointyEven, 1.0f); +``` + +Grids layouts and orientations are merged to one enum [HexagonalGridType](src/HexagonalLib/HexagonalGridType.cs). + +* PointyOdd - Horizontal layout shoves odd rows right [odd-r] +* PointyEven - Horizontal layout shoves even rows right [even-r] +* FlatOdd - Vertical layout shoves odd columns down [odd-q] +* FlatEven - Vertical layout shoves even columns down [even-q] + + +## Coordinates systems + +There is three coordinates systems represented in lib: + +* [Offset](src/HexagonalLib/Coordinates/Offset.cs) - offset coordinates ([link](https://www.redblobgames.com/grids/hexagons/#coordinates-offset)) +* [Cubic](src/HexagonalLib/Coordinates/Cubic.cs) - cube coordinates ([link](https://www.redblobgames.com/grids/hexagons/#coordinates-cube)) +* [Axial](src/HexagonalLib/Coordinates/Axial.cs) - axial coordinates ([link](https://www.redblobgames.com/grids/hexagons/#coordinates-axial)) + +_Note: there is no [Doubled](https://www.redblobgames.com/grids/hexagons/#coordinates-doubled) coordinates yet. I will add them in later versions._ + + +Struct [HexagonalGrid](src/HexagonalLib/HexagonalGrid.cs) contains a bunch of methods for coordinates conversion (all details are [described](https://www.redblobgames.com/grids/hexagons/#conversions) in original article): + +```C# +var offsetFromCubic = grid.ToOffset(cubic); +var offsetFromAxial = grid.ToOffset(axial); + +var cubicFromOffset = grid.ToCubic(offset); +var cubicFromAxial = grid.ToCubic(axial); + +var axialFromOffset = grid.ToAxial(offset); +var axialFromCubic = grid.ToAxial(cubic); +``` + +## Neighbors + +There is the order of neighbors of hex: + +![](img/hex-neighbors-order.png) + +You can take a neighbor of any hex by providing a coordinate of hex and required neighbor index. + +```C# +var oNeighbor = grid.GetNeighbor(offset, neighborIndex); +var cNeighbor = grid.GetNeighbor(cubic, neighborIndex); +var aNeighbor = grid.GetNeighbor(axial, neighborIndex); +``` +_The neighbor index can be negative or greater than 5 (it will be normalized)_ + +Or you can take all neighbors of particular hex + +```C# +var oNeighbors = grid.GetNeighbors(offset); +var cNeighbors = grid.GetNeighbors(cubic); +var aNeighbors = grid.GetNeighbors(axial); +``` + +There is also option to check are two hexes neighbors or not: + +```C# +var isNeighbors1 = grid.IsNeighbors(offset, oNeighbor); +var isNeighbors2 = grid.IsNeighbors(cubic, cNeighbor); +var isNeighbors3 = grid.IsNeighbors(axial, aNeighbor); +``` + +### Ring of neighbors +You also can get ring of neighbor for particular hex + +![](img/hex-rings.png) + +```C# +var oNeighbors = grid.GetNeighborsRing(offset, radius); +var cNeighbors = grid.GetNeighborsRing(cubic, radius); +var aNeighbors = grid.GetNeighborsRing(axial, radius); +``` + +### Circle of neighbors +Same as ring but you will receive hexes from all rings + +![](img/hex-around.png) + +```C# +var oNeighbors = grid.GetNeighborsAround(offset, radius); +var cNeighbors = grid.GetNeighborsAround(cubic, radius); +var aNeighbors = grid.GetNeighborsAround(axial, radius); +``` + +## 2D space + +There is also list of methods to convert hex coordinate to its center (point represented as tuple): + +```C# +var pointFromOffset = grid.ToPoint2(offset); +var pointFromCubic = grid.ToPoint2(cubic); +var pointFromAxial = grid.ToPoint2(axial); +``` + +To convert point into a hex coordinate which contains this point you can use: + +```C# +var offsetFromPoint = grid.ToOffset(x, y); +var cubicFromPoint = grid.ToCubic(x, y); +var axialFromPoint = grid.ToAxial(x, y); +``` + +Hexagons have 6 sides and 6 corners. There is corners order of the hexes: + +![](img/hex-corners-order.png) + +To take them in code simly use: + +```C# +var cornerFromOffset = grid.GetCorner(offset, cornerIndex); +var cornerFromCubic = grid.GetCorner(cubic, cornerIndex); +var cornerFromAxial = grid.GetCorner(axial, cornerIndex); +``` +_The corner index can be negative or greater than 5 (it will be normalized)_ \ No newline at end of file diff --git a/Src/HexagonalLib/img/hex-around.png b/Src/HexagonalLib/img/hex-around.png new file mode 100644 index 0000000000000000000000000000000000000000..10b0e8a078912de64b9cb659dea81f574bd0baf4 GIT binary patch literal 45298 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 literal 0 HcmV?d00001 diff --git a/Src/HexagonalLib/img/hex-corners-order.png b/Src/HexagonalLib/img/hex-corners-order.png new file mode 100644 index 0000000000000000000000000000000000000000..403a092089e97ee4ab1d9f0805c945d638754630 GIT binary patch literal 10462 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 literal 0 HcmV?d00001 diff --git a/Src/HexagonalLib/img/hex-neighbors-order.png b/Src/HexagonalLib/img/hex-neighbors-order.png new file mode 100644 index 0000000000000000000000000000000000000000..696f9acb5cc182576de95f62a1c01d8f60686c3c GIT binary patch literal 12766 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 0.5f; + private float DescribedRadius => (float) (InscribedRadius / Cos(PI / HexagonalGrid.EdgesCount)); + + [Test(Author = "Ivan Murashka", Description = "Check initial properties values for flat grids after creation")] + [TestCase(HexagonalGridType.FlatEven)] + [TestCase(HexagonalGridType.FlatOdd)] + public void FlatPropertiesTest(HexagonalGridType type) + { + var grid = new HexagonalGrid(type, InscribedRadius); + Assert.AreEqual(grid.InscribedRadius, InscribedRadius); + Assert.AreEqual(grid.DescribedRadius, DescribedRadius); + Assert.AreEqual(grid.InscribedDiameter, InscribedRadius * 2); + Assert.AreEqual(grid.DescribedDiameter, DescribedRadius * 2); + Assert.AreEqual(grid.HorizontalOffset, DescribedRadius * 1.5f); + Assert.AreEqual(grid.VerticalOffset, InscribedRadius * 2.0f); + Assert.AreEqual(grid.Side, DescribedRadius); + Assert.AreEqual(grid.AngleToFirstNeighbor, 30.0f); + } + + [Test(Author = "Ivan Murashka", Description = "Check initial properties values for pointy grids after creation")] + [TestCase(HexagonalGridType.PointyEven)] + [TestCase(HexagonalGridType.PointyOdd)] + public void PointyPropertiesTest(HexagonalGridType type) + { + var grid = new HexagonalGrid(type, InscribedRadius); + Assert.AreEqual(grid.InscribedRadius, InscribedRadius); + Assert.AreEqual(grid.DescribedRadius, DescribedRadius); + Assert.AreEqual(grid.InscribedDiameter, InscribedRadius * 2); + Assert.AreEqual(grid.DescribedDiameter, DescribedRadius * 2); + Assert.AreEqual(grid.HorizontalOffset, InscribedRadius * 2.0f); + Assert.AreEqual(grid.VerticalOffset, DescribedRadius * 1.5f); + Assert.AreEqual(grid.Side, DescribedRadius); + Assert.AreEqual(grid.AngleToFirstNeighbor, 0.0f); + } + + [Test(Author = "Ivan Murashka", Description = "Check coordinate conversion from Offset")] + public void CoordinateConversionTest( + [Values] HexagonalGridType type, + [Values(-13, -8, 0, 15, 22)] int offsetX, + [Values(-13, -8, 0, 15, 22)] int offsetY) + { + var grid = new HexagonalGrid(type, InscribedRadius); + var offset = new Offset(offsetX, offsetY); + var axial = grid.ToAxial(offset); + var cubic = grid.ToCubic(offset); + Assert.IsTrue(cubic.IsValid(), $"Invalid cubic coordinate: {cubic.X}-{cubic.Y}-{cubic.Z}"); + Assert.AreEqual(offset, grid.ToOffset(axial)); + Assert.AreEqual(offset, grid.ToOffset(cubic)); + Assert.AreEqual(axial, grid.ToAxial(offset)); + Assert.AreEqual(axial, grid.ToAxial(cubic)); + Assert.AreEqual(cubic, grid.ToCubic(offset)); + Assert.AreEqual(cubic, grid.ToCubic(axial)); + } + + [Test(Author = "Ivan Murashka", Description = "Check conversion to Point2 from Offset")] + public void PointConversionTest( + [Values] HexagonalGridType type, + [Values(-13, -8, 0, 15, 22)] int offsetX, + [Values(-13, -8, 0, 15, 22)] int offsetY) + { + var grid = new HexagonalGrid(type, InscribedRadius); + var offset = new Offset(offsetX, offsetY); + var axial = grid.ToAxial(offset); + var cubic = grid.ToCubic(offset); + + var fromOffset = grid.ToPoint2(offset); + var fromAxial = grid.ToPoint2(axial); + var fromCubic = grid.ToPoint2(cubic); + + Assert.IsTrue(fromOffset.SimilarTo(fromAxial), $"Expected: {fromAxial.X}:{fromAxial.Y}; Actual: {fromOffset.X}:{fromOffset.Y}"); + Assert.IsTrue(fromOffset.SimilarTo(fromCubic), $"Expected: {fromCubic.X}:{fromCubic.Y}; Actual: {fromOffset.X}:{fromOffset.Y}"); + + Assert.AreEqual(offset, grid.ToOffset(fromOffset)); + Assert.AreEqual(axial, grid.ToAxial(fromAxial)); + Assert.AreEqual(cubic, grid.ToCubic(fromCubic)); + } + + [Test(Author = "Ivan Murashka", Description = "Check IsNeighbor methods for all coordinates types")] + public void IsNeighborTest( + [Values] HexagonalGridType type, + [Values(-13, -8, 0, 15, 22)] int offsetX, + [Values(-13, -8, 0, 15, 22)] int offsetY, + [Values(-1, 0, 1, 2, 3, 4, 5, 6)] int neighborIndex) + { + var grid = new HexagonalGrid(type, InscribedRadius); + var offset = new Offset(offsetX, offsetY); + var axial = grid.ToAxial(offset); + var cubic = grid.ToCubic(offset); + Assert.IsTrue(cubic.IsValid(), $"Invalid cubic coordinate: {cubic.X}-{cubic.Y}-{cubic.Z}"); + + var oNeighbor = grid.GetNeighbor(offset, neighborIndex); + var aNeighbor = grid.GetNeighbor(axial, neighborIndex); + var cNeighbor = grid.GetNeighbor(cubic, neighborIndex); + + Assert.IsTrue(cNeighbor.IsValid(), $"Invalid cubic coordinate: {cNeighbor.X}-{cNeighbor.Y}-{cNeighbor.Z}"); + Assert.IsTrue(grid.IsNeighbors(offset, oNeighbor), $"Neighbor1={offset}; Neighbor2={oNeighbor}; Index={neighborIndex};"); + Assert.IsTrue(grid.IsNeighbors(axial, aNeighbor), $"Neighbor1={axial}; Neighbor2={aNeighbor}; Index={neighborIndex};"); + Assert.IsTrue(grid.IsNeighbors(cubic, cNeighbor), $"Neighbor1={cubic}; Neighbor2={cNeighbor}; Index={neighborIndex};"); + } + + [Test(Author = "Ivan Murashka", Description = "Check neighbors order for all coordinates types")] + public void NeighborsOrderTest( + [Values] HexagonalGridType type, + [Values(-13, -8, 0, 15, 22)] int offsetX, + [Values(-13, -8, 0, 15, 22)] int offsetY, + [Values(-1, 0, 1, 2, 3, 4, 5, 6)] int neighborIndex) + { + var grid = new HexagonalGrid(type, InscribedRadius); + var offset = new Offset(offsetX, offsetY); + var axial = grid.ToAxial(offset); + var cubic = grid.ToCubic(offset); + + var oNeighbor = grid.GetNeighbor(offset, neighborIndex); + var aNeighbor = grid.GetNeighbor(axial, neighborIndex); + var cNeighbor = grid.GetNeighbor(cubic, neighborIndex); + + Assert.IsTrue(cNeighbor.IsValid(), $"Invalid cubic coordinate: {cNeighbor.X}-{cNeighbor.Y}-{cNeighbor.Z}"); + + var fromAxial = grid.ToOffset(aNeighbor); + var fromCubic = grid.ToOffset(cNeighbor); + + Assert.AreEqual(oNeighbor, fromAxial, $"Center=({offset} - {axial}); Current=({oNeighbor} - {aNeighbor}); Index={neighborIndex};"); + Assert.AreEqual(oNeighbor, fromCubic, $"Center=({offset} - {cubic}); Current=({oNeighbor} - {cNeighbor}); Index={neighborIndex};"); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalLib.Net.Tests.csproj b/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalLib.Net.Tests.csproj new file mode 100644 index 0000000..ce25b27 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalLib.Net.Tests.csproj @@ -0,0 +1,26 @@ + + + + HexagonalLib.Tests + false + net472;net48;netcoreapp3.1 + + + + ..\..\.bin\HexagonalLib.Net.Tests\Debug\ + + + + ..\..\.bin\HexagonalLib.Net.Tests\Release\ + + + + + + + + + + + + diff --git a/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalMathTests.cs b/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalMathTests.cs new file mode 100644 index 0000000..eb7f9e3 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net.Tests/HexagonalMathTests.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; + +namespace HexagonalLib.Tests +{ + [TestFixture(TestOf = typeof(HexagonalMath))] + public class HexagonalMathTests + { + [Test(Author = "Ivan Murashka", Description = "Check HexagonalMath.Rotate method")] + [TestCase(0, 1, 0, 0, 1)] + [TestCase(0, 1, 45, 0.5f, 0.5f)] + [TestCase(0, 1, 90, 1, 0)] + [TestCase(0, 1, 135, 0.5f, -0.5f)] + [TestCase(0, 1, 180, 0, -1)] + [TestCase(0, 1, 225, -0.5f, -0.5f)] + [TestCase(0, 1, 270, -1, 0)] + [TestCase(0, 1, 315, -0.5f, 0.5f)] + [TestCase(0, 1, 360, 0, 1)] + public void RotateTest(float x, float y, float degrees, float resultX, float resultY) + { + var vector = (x, y).Rotate(degrees); + var result = (resultX, resultY).Normalize(); + Assert.IsTrue(vector.SimilarTo(result), $"Expected: {result.X}:{result.Y}; Actual: {vector.X}:{vector.Y}"); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net/HexagonalLib.Net.csproj b/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net/HexagonalLib.Net.csproj new file mode 100644 index 0000000..400ca8b --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Net/HexagonalLib.Net/HexagonalLib.Net.csproj @@ -0,0 +1,23 @@ + + + + HexagonalLib + net472;net48;netcoreapp3.1 + true + HexagonalLib + Ivan Murashka + 1.0.0 + https://github.com/imurashka/HexagonalLib + + + + ..\..\.bin\HexagonalLib.Net\Debug\ + + + + ..\..\.bin\HexagonalLib.Net\Release\ + + + + + diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalGridTests.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalGridTests.cs new file mode 100644 index 0000000..d5de6c4 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalGridTests.cs @@ -0,0 +1,57 @@ +using HexagonalLib.Coordinates; +using NUnit.Framework; + +namespace HexagonalLib.Tests +{ + [TestFixture] + public class HexagonalGridTests + { + private float InscribedRadius => 0.5f; + + [Test(Author = "Ivan Murashka", Description = "Check coordinate conversion from Offset to Unity vectors")] + public void Vector2ConversionTest( + [Values] HexagonalGridType type, + [Values(-13, -8, 0, 15, 22)] int offsetX, + [Values(-13, -8, 0, 15, 22)] int offsetY) + { + var grid = new HexagonalGrid(type, InscribedRadius); + var offset = new Offset(offsetX, offsetY); + var axial = grid.ToAxial(offset); + var cubic = grid.ToCubic(offset); + + var fromOffset = grid.ToVector2(offset); + var fromAxial = grid.ToVector2(axial); + var fromCubic = grid.ToVector2(cubic); + + Assert.IsTrue(fromOffset.SimilarTo(fromAxial), $"Expected: {fromAxial}; Actual: {fromOffset}"); + Assert.IsTrue(fromOffset.SimilarTo(fromCubic), $"Expected: {fromCubic}; Actual: {fromOffset}"); + + Assert.AreEqual(offset, grid.ToOffset(fromOffset)); + Assert.AreEqual(axial, grid.ToAxial(fromAxial)); + Assert.AreEqual(cubic, grid.ToCubic(fromCubic)); + } + + [Test(Author = "Ivan Murashka", Description = "Check coordinate conversion from Offset to Unity vectors")] + public void Vector3ConversionTest( + [Values] HexagonalGridType type, + [Values(-13, -8, 0, 15, 22)] int offsetX, + [Values(-13, -8, 0, 15, 22)] int offsetY) + { + var grid = new HexagonalGrid(type, InscribedRadius); + var offset = new Offset(offsetX, offsetY); + var axial = grid.ToAxial(offset); + var cubic = grid.ToCubic(offset); + + var fromOffset = grid.ToVector3(offset); + var fromAxial = grid.ToVector3(axial); + var fromCubic = grid.ToVector3(cubic); + + Assert.IsTrue(fromOffset.SimilarTo(fromAxial), $"Expected: {fromAxial}; Actual: {fromOffset}"); + Assert.IsTrue(fromOffset.SimilarTo(fromCubic), $"Expected: {fromCubic}; Actual: {fromOffset}"); + + Assert.AreEqual(offset, grid.ToOffset(fromOffset)); + Assert.AreEqual(axial, grid.ToAxial(fromAxial)); + Assert.AreEqual(cubic, grid.ToCubic(fromCubic)); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalLib.Unity.Tests.csproj b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalLib.Unity.Tests.csproj new file mode 100644 index 0000000..a813ad0 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/HexagonalLib.Unity.Tests.csproj @@ -0,0 +1,78 @@ + + + + + + Debug + AnyCPU + {1E807875-014C-4745-86AB-E1E35916F8B3} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + HexagonalLib.Tests + HexagonalLib.Unity.Tests + v4.7.2 + 512 + + + AnyCPU + true + full + false + ..\..\.bin\HexagonalLib.Unity.Tests\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\..\.bin\HexagonalLib.Unity.Tests\Debug\ + TRACE + prompt + 4 + + + + ..\..\.packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + True + + + + + + + ..\..\.packages\Unity3D.UnityEngine.2018.3.5.1\lib\UnityEngine.dll + True + + + + + + + + + {ee497232-ac7a-48d0-9059-53b152c6bf01} + HexagonalLib.Unity + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + + diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/Properties/AssemblyInfo.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2436b4e --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("HexagonalLib.Unity.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("HexagonalLib.Unity.Tests")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1E807875-014C-4745-86AB-E1E35916F8B3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/packages.config b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/packages.config new file mode 100644 index 0000000..5fff8f5 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Axial.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Axial.cs new file mode 100644 index 0000000..6023081 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Axial.cs @@ -0,0 +1,7 @@ +namespace HexagonalLib.Coordinates +{ + public partial struct Axial + { + + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Cubic.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Cubic.cs new file mode 100644 index 0000000..10fae70 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Cubic.cs @@ -0,0 +1,6 @@ +namespace HexagonalLib.Coordinates +{ + public readonly partial struct Cubic + { + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Offset.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Offset.cs new file mode 100644 index 0000000..6548187 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Coordinates/Offset.cs @@ -0,0 +1,6 @@ +namespace HexagonalLib.Coordinates +{ + public readonly partial struct Offset + { + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalGrid.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalGrid.cs new file mode 100644 index 0000000..a8c0c47 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalGrid.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using HexagonalLib.Coordinates; +using HexagonalLib.Utility; +using UnityEngine; + +namespace HexagonalLib +{ + public partial struct HexagonalGrid + { + #region ToOffset + + ///

+ /// Convert point to offset coordinate + /// + public Offset ToOffset(Vector2 vector) + { + return ToOffset(vector.AsTuple()); + } + + /// + /// Convert point to offset coordinate + /// + public Offset ToOffset(Vector3 vector) + { + return ToOffset(vector.AsTuple()); + } + + #endregion + + #region ToAxial + + /// + /// Convert point to axial coordinate + /// + public Axial ToAxial(Vector2 vector) + { + return ToAxial(vector.AsTuple()); + } + + /// + /// Convert point to axial coordinate + /// + public Axial ToAxial(Vector3 vector) + { + return ToAxial(vector.AsTuple()); + } + + #endregion + + #region ToCubic + + /// + /// Convert point to cubic coordinate + /// + public Cubic ToCubic(Vector2 vector) + { + return ToCubic(vector.AsTuple()); + } + + /// + /// Convert point to cubic coordinate + /// + public Cubic ToCubic(Vector3 vector) + { + return ToCubic(vector.AsTuple()); + } + + #endregion + + #region ToVector + + /// + /// Convert hex based on its offset coordinate to it center position in 2d space + /// + public Vector2 ToVector2(Offset coord) + { + return ToPoint2(coord).AsVector2(); + } + + /// + /// Convert hex based on its axial coordinate to it center position in 2d space + /// + public Vector2 ToVector2(Axial coord) + { + return ToPoint2(coord).AsVector2(); + } + + /// + /// Convert hex based on its cubic coordinate to it center position in 2d space + /// + public Vector2 ToVector2(Cubic coord) + { + return ToPoint2(coord).AsVector2(); + } + + /// + /// Convert hex based on its offset coordinate to it center position in 3d space OZ + /// + public Vector3 ToVector3(Offset coord, float y = 0) + { + return ToPoint2(coord).AsVector3(y); + } + + /// + /// Convert hex based on its axial coordinate to it center position in 3d space OZ + /// + public Vector3 ToVector3(Axial coord, float y = 0) + { + return ToPoint2(coord).AsVector3(y); + } + + /// + /// Convert hex based on its cubic coordinate to it center position in 3d space OZ + /// + public Vector3 ToVector3(Cubic coord, float y = 0) + { + return ToPoint2(coord).AsVector3(y); + } + + #endregion + + #region CreateMesh + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// + public Mesh CreateMesh(IReadOnlyList hexes, int subdivide) + { + return CreateMesh(hexes, subdivide, CreateMesh); + } + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// + public Mesh CreateMesh(IReadOnlyList hexes, int subdivide) + { + return CreateMesh(hexes, subdivide, CreateMesh); + } + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// + public Mesh CreateMesh(IReadOnlyList hexes, int subdivide) + { + return CreateMesh(hexes, subdivide, CreateMesh); + } + + private Mesh CreateMesh(IReadOnlyList hexes, int subdivide, Action, int, Action, Action> createMesh) + { + var meshData = GetMeshData(hexes.Count, subdivide); + var vertices = new Vector3[meshData.VerticesCount]; + var triangles = new int[meshData.IndicesCount]; + + createMesh(hexes, subdivide, + (i, point) => vertices[i] = new Vector3(point.X, 0, point.Y), + (i, index) => triangles[i] = index); + + var mesh = new Mesh + { + vertices = vertices, + triangles = triangles + }; + + mesh.RecalculateNormals(); + return mesh; + } + + #endregion + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalLib.Unity.csproj b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalLib.Unity.csproj new file mode 100644 index 0000000..fdbc0f8 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalLib.Unity.csproj @@ -0,0 +1,66 @@ + + + + + Debug + AnyCPU + {EE497232-AC7A-48D0-9059-53B152C6BF01} + Library + Properties + HexagonalLib + HexagonalLib.Unity + v4.7.2 + 512 + + + AnyCPU + true + full + false + ..\..\.bin\HexagonalLib.Unity\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\..\.bin\HexagonalLib.Unity\Release\ + TRACE + prompt + 4 + + + + + + + + ..\..\.packages\Unity3D.UnityEngine.2018.3.5.1\lib\UnityEngine.dll + True + + + + + + + + + + + + + + + + + + + diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalMath.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalMath.cs new file mode 100644 index 0000000..fbd913d --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/HexagonalMath.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +namespace HexagonalLib +{ + public static partial class HexagonalMath + { + /// + /// Compares two vectors and returns true if they are similar. + /// + public static bool SimilarTo(this in Vector2 a, in Vector2 b) + { + return a.x.SimilarTo(b.x) && a.y.SimilarTo(b.y); + } + + /// + /// Compares two vectors and returns true if they are similar. + /// + public static bool SimilarTo(this in Vector3 a, in Vector3 b) + { + return a.x.SimilarTo(b.x) && a.y.SimilarTo(b.y) && a.z.SimilarTo(b.z); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Properties/AssemblyInfo.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..156538b --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("HexagonalLib.Unity")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("HexagonalLib.Unity")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("EE497232-AC7A-48D0-9059-53B152C6BF01")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Utility/VectorUtility.cs b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Utility/VectorUtility.cs new file mode 100644 index 0000000..e4822e9 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/Utility/VectorUtility.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace HexagonalLib.Utility +{ + internal static class VectorUtility + { + internal static (float x, float y) AsTuple(this Vector2 vector) + { + return (vector.x, vector.y); + } + + internal static (float x, float y) AsTuple(this Vector3 vector) + { + return (vector.x, vector.z); + } + + internal static Vector2 AsVector2(this (float x, float y) tuple) + { + return new Vector2(tuple.x, tuple.y); + } + + internal static Vector3 AsVector3(this (float x, float y) tuple, float y = 0.0f) + { + return new Vector3(tuple.x, y, tuple.y); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/packages.config b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/packages.config new file mode 100644 index 0000000..986d258 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.Unity/HexagonalLib.Unity/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib.sln b/Src/HexagonalLib/src/HexagonalLib.sln new file mode 100644 index 0000000..b6ceba8 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "HexagonalLib", "HexagonalLib\HexagonalLib.shproj", "{30905D5D-8D2E-4A25-ABEB-B4B4B0C440E6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HexagonalLib.Unity", "HexagonalLib.Unity", "{C02A807F-4B02-4900-895F-67E25F8CF774}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HexagonalLib.Unity.Tests", "HexagonalLib.Unity\HexagonalLib.Unity.Tests\HexagonalLib.Unity.Tests.csproj", "{1E807875-014C-4745-86AB-E1E35916F8B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HexagonalLib.Unity", "HexagonalLib.Unity\HexagonalLib.Unity\HexagonalLib.Unity.csproj", "{EE497232-AC7A-48D0-9059-53B152C6BF01}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HexagonalLib.Net", "HexagonalLib.Net", "{1F507917-8D29-4061-BC98-7B64DABADED3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HexagonalLib.Net.Tests", "HexagonalLib.Net\HexagonalLib.Net.Tests\HexagonalLib.Net.Tests.csproj", "{2608E591-E33C-446A-9E95-AFED738B2512}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HexagonalLib.Net", "HexagonalLib.Net\HexagonalLib.Net\HexagonalLib.Net.csproj", "{CF391253-3550-40CF-810B-A6F8FCA2EBF2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {30905D5D-8D2E-4A25-ABEB-B4B4B0C440E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30905D5D-8D2E-4A25-ABEB-B4B4B0C440E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE497232-AC7A-48D0-9059-53B152C6BF01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE497232-AC7A-48D0-9059-53B152C6BF01}.Release|Any CPU.Build.0 = Release|Any CPU + {EE497232-AC7A-48D0-9059-53B152C6BF01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE497232-AC7A-48D0-9059-53B152C6BF01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E807875-014C-4745-86AB-E1E35916F8B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E807875-014C-4745-86AB-E1E35916F8B3}.Release|Any CPU.Build.0 = Release|Any CPU + {1E807875-014C-4745-86AB-E1E35916F8B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E807875-014C-4745-86AB-E1E35916F8B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF391253-3550-40CF-810B-A6F8FCA2EBF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF391253-3550-40CF-810B-A6F8FCA2EBF2}.Release|Any CPU.Build.0 = Release|Any CPU + {CF391253-3550-40CF-810B-A6F8FCA2EBF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF391253-3550-40CF-810B-A6F8FCA2EBF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2608E591-E33C-446A-9E95-AFED738B2512}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2608E591-E33C-446A-9E95-AFED738B2512}.Release|Any CPU.Build.0 = Release|Any CPU + {2608E591-E33C-446A-9E95-AFED738B2512}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2608E591-E33C-446A-9E95-AFED738B2512}.Debug|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1E807875-014C-4745-86AB-E1E35916F8B3} = {C02A807F-4B02-4900-895F-67E25F8CF774} + {EE497232-AC7A-48D0-9059-53B152C6BF01} = {C02A807F-4B02-4900-895F-67E25F8CF774} + {CF391253-3550-40CF-810B-A6F8FCA2EBF2} = {1F507917-8D29-4061-BC98-7B64DABADED3} + {2608E591-E33C-446A-9E95-AFED738B2512} = {1F507917-8D29-4061-BC98-7B64DABADED3} + EndGlobalSection +EndGlobal diff --git a/Src/HexagonalLib/src/HexagonalLib/Coordinates/Axial.cs b/Src/HexagonalLib/src/HexagonalLib/Coordinates/Axial.cs new file mode 100644 index 0000000..9a50817 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/Coordinates/Axial.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace HexagonalLib.Coordinates +{ + [Serializable] + public readonly partial struct Axial : IEquatable, IEqualityComparer + { + public static Axial Zero => new Axial(0, 0); + + public readonly int Q; + public readonly int R; + + public Axial(int q, int r) + : this() + { + Q = q; + R = r; + } + + public static bool operator ==(Axial coord1, Axial coord2) + { + return (coord1.Q, coord1.R) == (coord2.Q, coord2.R); + } + + public static bool operator !=(Axial coord1, Axial coord2) + { + return (coord1.Q, coord1.R) != (coord2.Q, coord2.R); + } + + public static Axial operator +(Axial coord1, Axial coord2) + { + return new Axial(coord1.Q + coord2.Q, coord1.R + coord2.R); + } + + public static Axial operator +(Axial coord, int offset) + { + return new Axial(coord.Q + offset, coord.R + offset); + } + + public static Axial operator -(Axial coord1, Axial coord2) + { + return new Axial(coord1.Q - coord2.Q, coord1.R - coord2.R); + } + + public static Axial operator -(Axial coord, int offset) + { + return new Axial(coord.Q - offset, coord.R - offset); + } + + public static Axial operator *(Axial coord, int offset) + { + return new Axial(coord.Q * offset, coord.R * offset); + } + + public override bool Equals(object other) + { + return other is Axial axial && Equals(axial); + } + + public bool Equals(Axial other) + { + return (Q, R) == (other.Q, other.R); + } + + public bool Equals(Axial coord1, Axial coord2) + { + return coord1.Equals(coord2); + } + + public override int GetHashCode() + { + return (Q, R).GetHashCode(); + } + + public int GetHashCode(Axial axial) + { + return axial.GetHashCode(); + } + + public override string ToString() + { + return $"A-[{Q}:{R}]"; + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/Coordinates/Cubic.cs b/Src/HexagonalLib/src/HexagonalLib/Coordinates/Cubic.cs new file mode 100644 index 0000000..a03b2d2 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/Coordinates/Cubic.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; + +namespace HexagonalLib.Coordinates +{ + [Serializable] + public readonly partial struct Cubic : IEquatable, IEqualityComparer + { + public static Cubic Zero => new Cubic(0, 0, 0); + + public readonly int X; + public readonly int Y; + public readonly int Z; + + public Cubic(int x, int y, int z) + : this() + { + X = x; + Y = y; + Z = z; + } + + /// + /// Round float coordinates to nearest valid coordinate + /// + public Cubic(float x, float y, float z) + { + var rx = (int) Math.Round(x); + var ry = (int) Math.Round(y); + var rz = (int) Math.Round(z); + + var xDiff = Math.Abs(rx - x); + var yDiff = Math.Abs(ry - y); + var zDiff = Math.Abs(rz - z); + + if (xDiff > yDiff && xDiff > zDiff) + { + rx = -ry - rz; + } + else if (yDiff > zDiff) + { + ry = -rx - rz; + } + else + { + rz = -rx - ry; + } + + X = rx; + Y = ry; + Z = rz; + } + + public static bool operator ==(Cubic coord1, Cubic coord2) + { + return (coord1.X, coord1.Y, coord1.Z) == (coord2.X, coord2.Y, coord2.Z); + } + + public static bool operator !=(Cubic coord1, Cubic coord2) + { + return (coord1.X, coord1.Y, coord1.Z) != (coord2.X, coord2.Y, coord2.Z); + } + + public static Cubic operator +(Cubic coord1, Cubic coord2) + { + return new Cubic(coord1.X + coord2.X, coord1.Y + coord2.Y, coord1.Z + coord2.Z); + } + + public static Cubic operator +(Cubic coord, int offset) + { + return new Cubic(coord.X + offset, coord.Y + offset, coord.Z + offset); + } + + public static Cubic operator -(Cubic coord1, Cubic coord2) + { + return new Cubic(coord1.X - coord2.X, coord1.Y - coord2.Y, coord1.Z - coord2.Z); + } + + public static Cubic operator -(Cubic coord, int offset) + { + return new Cubic(coord.X - offset, coord.Y - offset, coord.Z - offset); + } + + public static Cubic operator *(Cubic coord, int offset) + { + return new Cubic(coord.X * offset, coord.Y * offset, coord.Z * offset); + } + + public static Cubic operator *(Cubic coord, float delta) + { + return new Cubic(coord.X * delta, coord.Y * delta, coord.Z * delta); + } + + public bool IsValid() + { + return X + Y + Z == 0; + } + + public Cubic RotateToRight() + { + var x = -Y; + var y = -Z; + var z = -X; + return new Cubic(x, y, z); + } + + public Cubic RotateToRight(int times) + { + var cur = this; + for (var i = 0; i < times; i++) + { + cur = cur.RotateToRight(); + } + + return cur; + } + + public override bool Equals(object obj) + { + return obj is Cubic other && Equals(other); + } + + public bool Equals(Cubic other) + { + return (X, Y, Z) == (other.X, other.Y, other.Z); + } + + public bool Equals(Cubic coord1, Cubic coord2) + { + return coord1.Equals(coord2); + } + + public override int GetHashCode() + { + return (X, Y, Z).GetHashCode(); + } + + public int GetHashCode(Cubic coord) + { + return coord.GetHashCode(); + } + + public override string ToString() + { + return !IsValid() ? "C-[Invalid]" : $"C-[{X}:{Y}:{Z}]"; + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/Coordinates/Offset.cs b/Src/HexagonalLib/src/HexagonalLib/Coordinates/Offset.cs new file mode 100644 index 0000000..2725aa8 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/Coordinates/Offset.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +namespace HexagonalLib.Coordinates +{ + [Serializable] + public readonly partial struct Offset : IEquatable, IEqualityComparer + { + public static Offset Zero => new Offset(0, 0); + + public readonly int X; + public readonly int Y; + + public Offset(int x, int y) + : this() + { + X = x; + Y = y; + } + + public Offset Add(int xOffset, int yOffset) + { + return new Offset(X + xOffset, Y + yOffset); + } + + public static Offset Clamp(Offset coord, Offset min, Offset max) + { + var x = Clamp(coord.X, min.X, max.X); + var y = Clamp(coord.Y, min.Y, max.Y); + + return new Offset(x, y); + } + + private static int Clamp(int value, int min, int max) + { + if (value < min) + { + value = min; + } + else if (value > max) + { + value = max; + } + + return value; + } + + public static bool operator ==(Offset coord1, Offset coord2) + { + return (coord1.X, coord1.Y) == (coord2.X, coord2.Y); + } + + public static bool operator !=(Offset coord1, Offset coord2) + { + return (coord1.X, coord1.Y) != (coord2.X, coord2.Y); + } + + public static Offset operator +(Offset coord1, Offset coord2) + { + return new Offset(coord1.X + coord2.X, coord1.Y + coord2.Y); + } + + public static Offset operator +(Offset coord, int offset) + { + return new Offset(coord.X + offset, coord.Y + offset); + } + + public static Offset operator -(Offset coord, Offset index2) + { + return new Offset(coord.X - index2.X, coord.Y - index2.Y); + } + + public static Offset operator /(Offset coord, int value) + { + return new Offset(coord.X / value, coord.Y / value); + } + + public static Offset operator *(Offset coord, int offset) + { + return new Offset(coord.X * offset, coord.Y * offset); + } + + public override bool Equals(object obj) + { + return obj is Offset other && Equals(other); + } + + public bool Equals(Offset other) + { + return (X, Y) == (other.X, other.Y); + } + + public bool Equals(Offset coord1, Offset coord2) + { + return coord1.Equals(coord2); + } + + public override int GetHashCode() + { + return (X, Y).GetHashCode(); + } + + public int GetHashCode(Offset coord) + { + return coord.GetHashCode(); + } + + public override string ToString() + { + return $"O-[{X}:{Y}]"; + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalException.cs b/Src/HexagonalLib/src/HexagonalLib/HexagonalException.cs new file mode 100644 index 0000000..7d48b8b --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalException.cs @@ -0,0 +1,72 @@ +using System; +using System.Text; + +namespace HexagonalLib +{ + public class HexagonalException : Exception + { + private class MessageBuilder + { + private readonly StringBuilder _message = new StringBuilder(); + + public MessageBuilder Append(string message) + { + _message.AppendLine(message); + return this; + } + + public MessageBuilder Append(HexagonalGrid grid) + { + Append(nameof(grid.Type), grid.Type); + Append(nameof(grid.InscribedRadius), grid.InscribedRadius); + Append(nameof(grid.DescribedRadius), grid.DescribedRadius); + return this; + } + + public MessageBuilder Append(params (string, object)[] fields) + { + foreach (var (paramName, paramValue) in fields) + { + Append(paramName, paramValue); + } + + return this; + } + + private void Append(string paramName, object paramValue) + { + _message.Append($"{paramName}={paramValue}; "); + } + + public override string ToString() + { + return _message.ToString(); + } + } + + public HexagonalException(string message) + : base(message) + { + } + + public HexagonalException(string message, params (string, object)[] fields) + : base(CreateBuilder(message).Append(fields).ToString()) + { + } + + public HexagonalException(string message, HexagonalGrid grid) + : base(CreateBuilder(message).Append(grid).ToString()) + { + } + + public HexagonalException(string message, HexagonalGrid grid, params (string, object)[] fields) + : base(CreateBuilder(message).Append(grid).Append(fields).ToString()) + { + } + + private static MessageBuilder CreateBuilder(string message) + { + return new MessageBuilder().Append(message); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalGrid.cs b/Src/HexagonalLib/src/HexagonalLib/HexagonalGrid.cs new file mode 100644 index 0000000..ad67ee1 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalGrid.cs @@ -0,0 +1,812 @@ +using System; +using System.Collections.Generic; +using HexagonalLib.Coordinates; +using static System.Math; + +namespace HexagonalLib +{ + /// + /// Represent geometry logic for infinity hexagonal grid + /// + public readonly partial struct HexagonalGrid + { + /// + /// Total count of edges in one Hex + /// + public const int EdgesCount = 6; + + public static readonly float Sqrt3 = (float) Sqrt(3); + + /// + /// Inscribed radius of the hex + /// + public readonly float InscribedRadius; + + /// + /// Described radius of hex + /// + public readonly float DescribedRadius; + + /// + /// Hexagon side length + /// + public float Side => DescribedRadius; + + /// + /// Inscribed diameter of hex + /// + public float InscribedDiameter => InscribedRadius * 2; + + /// + /// Described diameter of hex + /// + public float DescribedDiameter => DescribedRadius * 2; + + /// + /// Orientation and layout of this grid + /// + public readonly HexagonalGridType Type; + + /// + /// Offset between hex and its right-side neighbour on X axis + /// + public float HorizontalOffset + { + get + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + case HexagonalGridType.PointyEven: + return InscribedRadius * 2.0f; + case HexagonalGridType.FlatOdd: + case HexagonalGridType.FlatEven: + return DescribedRadius * 1.5f; + default: + throw new HexagonalException($"Can't get {nameof(HorizontalOffset)} with unexpected {nameof(Type)}", this); + } + } + } + + /// + /// Offset between hex and its up-side neighbour on Y axis + /// + public float VerticalOffset + { + get + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + case HexagonalGridType.PointyEven: + return DescribedRadius * 1.5f; + case HexagonalGridType.FlatOdd: + case HexagonalGridType.FlatEven: + return InscribedRadius * 2.0f; + default: + throw new HexagonalException($"Can't get {nameof(VerticalOffset)} with unexpected {nameof(Type)}", this); + } + } + } + + /// + /// The angle between the centers of any hex and its first neighbor relative to the vector (0, 1) clockwise + /// + /// + public float AngleToFirstNeighbor + { + get + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + case HexagonalGridType.PointyEven: + return 0.0f; + case HexagonalGridType.FlatOdd: + case HexagonalGridType.FlatEven: + return 30.0f; + default: + throw new HexagonalException($"Can't get {nameof(AngleToFirstNeighbor)} with unexpected {nameof(Type)}", this); + } + } + } + + /// + /// Base constructor for hexagonal grid + /// + /// Orientation and layout of the grid + /// Inscribed radius + public HexagonalGrid(HexagonalGridType type, float radius) + { + Type = type; + InscribedRadius = radius; + DescribedRadius = (float) (radius / Cos(PI / EdgesCount)); + } + + #region ToOffset + + /// + /// Convert cubic coordinate to offset + /// + public Offset ToOffset(Cubic coord) + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + { + var col = coord.X + (coord.Z - (coord.Z & 1)) / 2; + var row = coord.Z; + return new Offset(col, row); + } + case HexagonalGridType.PointyEven: + { + var col = coord.X + (coord.Z + (coord.Z & 1)) / 2; + var row = coord.Z; + return new Offset(col, row); + } + case HexagonalGridType.FlatOdd: + { + var col = coord.X; + var row = coord.Z + (coord.X - (coord.X & 1)) / 2; + return new Offset(col, row); + } + case HexagonalGridType.FlatEven: + { + var col = coord.X; + var row = coord.Z + (coord.X + (coord.X & 1)) / 2; + return new Offset(col, row); + } + default: + throw new HexagonalException($"{nameof(ToOffset)} failed with unexpected {nameof(Type)}", this, (nameof(coord), coord)); + } + } + + /// + /// Convert axial coordinate to offset + /// + public Offset ToOffset(Axial axial) + { + return ToOffset(ToCubic(axial)); + } + + /// + /// Returns the offset coordinate of the hex which contains a point + /// + public Offset ToOffset(float x, float y) + { + return ToOffset(ToCubic(x, y)); + } + + /// + /// Returns the offset coordinate of the hex which contains a point + /// + public Offset ToOffset((float X, float Y) point) + { + return ToOffset(ToCubic(point.X, point.Y)); + } + + #endregion + + #region ToAxial + + /// + /// Convert cubic coordinate to axial + /// + public Axial ToAxial(Cubic cubic) + { + return new Axial(cubic.X, cubic.Z); + } + + /// + /// Convert offset coordinate to axial + /// + public Axial ToAxial(Offset offset) + { + return ToAxial(ToCubic(offset)); + } + + /// + /// Returns the axial coordinate of the hex which contains a point + /// + public Axial ToAxial(float x, float y) + { + return ToAxial(ToCubic(x, y)); + } + + /// + /// Returns the axial coordinate of the hex which contains a point + /// + public Axial ToAxial((float X, float Y) point) + { + return ToAxial(ToCubic(point.X, point.Y)); + } + + #endregion + + #region ToCubic + + /// + /// Convert offset coordinate to cubic + /// + public Cubic ToCubic(Offset coord) + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + { + var x = coord.X - (coord.Y - (coord.Y & 1)) / 2; + var z = coord.Y; + var y = -x - z; + return new Cubic(x, y, z); + } + case HexagonalGridType.PointyEven: + { + var x = coord.X - (coord.Y + (coord.Y & 1)) / 2; + var z = coord.Y; + var y = -x - z; + return new Cubic(x, y, z); + } + case HexagonalGridType.FlatOdd: + { + var x = coord.X; + var z = coord.Y - (coord.X - (coord.X & 1)) / 2; + var y = -x - z; + return new Cubic(x, y, z); + } + case HexagonalGridType.FlatEven: + { + var x = coord.X; + var z = coord.Y - (coord.X + (coord.X & 1)) / 2; + var y = -x - z; + return new Cubic(x, y, z); + } + default: + throw new HexagonalException($"{nameof(ToCubic)} failed with unexpected {nameof(Type)}", this, (nameof(coord), coord)); + } + } + + /// + /// Convert axial coordinate to cubic + /// + public Cubic ToCubic(Axial axial) + { + return new Cubic(axial.Q, -axial.Q - axial.R, axial.R); + } + + /// + /// Returns the cubic coordinate of the hex which contains a point + /// + public Cubic ToCubic(float x, float y) + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + case HexagonalGridType.PointyEven: + { + var q = (x * Sqrt3 / 3.0f - y / 3.0f) / Side; + var r = y * 2.0f / 3.0f / Side; + return new Cubic(q, -q - r, r); + } + case HexagonalGridType.FlatOdd: + case HexagonalGridType.FlatEven: + { + var q = x * 2.0f / 3.0f / Side; + var r = (-x / 3.0f + Sqrt3 / 3.0f * y) / Side; + return new Cubic(q, -q - r, r); + } + default: + throw new HexagonalException($"{nameof(ToCubic)} failed with unexpected {nameof(Type)}", this, (nameof(x), x), (nameof(y), y)); + } + } + + /// + /// Returns the cubic coordinate of the hex which contains a point + /// + public Cubic ToCubic((float X, float Y) point) + { + return ToCubic(point.X, point.Y); + } + + #endregion + + #region ToPoint2 + + /// + /// Convert hex based on its offset coordinate to it center position in 2d space + /// + public (float X, float Y) ToPoint2(Offset coord) + { + return ToPoint2(ToAxial(coord)); + } + + /// + /// Convert hex based on its axial coordinate to it center position in 2d space + /// + public (float X, float Y) ToPoint2(Axial coord) + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + case HexagonalGridType.PointyEven: + { + var x = Side * (Sqrt3 * coord.Q + Sqrt3 / 2 * coord.R); + var y = Side * (3.0f / 2.0f * coord.R); + return (x, y); + } + case HexagonalGridType.FlatOdd: + case HexagonalGridType.FlatEven: + { + var x = Side * (3.0f / 2.0f * coord.Q); + var y = Side * (Sqrt3 / 2 * coord.Q + Sqrt3 * coord.R); + return (x, y); + } + default: + throw new HexagonalException($"{nameof(ToPoint2)} failed with unexpected {nameof(Type)}", this, (nameof(coord), coord)); + } + } + + /// + /// Convert hex based on its cubic coordinate to it center position in 2d space + /// + public (float X, float Y) ToPoint2(Cubic coord) + { + return ToPoint2(ToAxial(coord)); + } + + #endregion + + #region GetCornerPoint + + /// + /// Returns corner point in 2d space of given coordinate + /// + public (float X, float Y) GetCornerPoint(Offset coord, int edge) + { + return GetCornerPoint(coord, edge, ToPoint2); + } + + /// + /// Returns corner point in 2d space of given coordinate + /// + public (float X, float Y) GetCornerPoint(Axial coord, int edge) + { + return GetCornerPoint(coord, edge, ToPoint2); + } + + /// + /// Returns corner point in 2d space of given coordinate + /// + public (float X, float Y) GetCornerPoint(Cubic coord, int edge) + { + return GetCornerPoint(coord, edge, ToPoint2); + } + + /// + /// Returns corner point in 2d space of given coordinate + /// + private (float X, float Y) GetCornerPoint(T coord, int edge, Func toPoint) + where T : struct + { + edge = NormalizeIndex(edge); + var angleDeg = 60 * edge; + if (Type == HexagonalGridType.PointyEven || Type == HexagonalGridType.PointyOdd) + { + angleDeg -= 30; + } + + var center = toPoint(coord); + var angleRad = PI / 180 * angleDeg; + var x = (float) (center.X + DescribedRadius * Cos(angleRad)); + var y = (float) (center.Y + DescribedRadius * Sin(angleRad)); + return (x, y); + } + + #endregion + + #region GetNeighbor + + /// + /// Returns the neighbor at the specified index. + /// + public Offset GetNeighbor(Offset coord, int neighborIndex) + { + return coord + GetNeighborsOffsets(coord)[NormalizeIndex(neighborIndex)]; + } + + /// + /// Returns the neighbor at the specified index. + /// + public Axial GetNeighbor(Axial coord, int neighborIndex) + { + return coord + _axialNeighbors[NormalizeIndex(neighborIndex)]; + } + + /// + /// Returns the neighbor at the specified index. + /// + public Cubic GetNeighbor(Cubic coord, int neighborIndex) + { + return coord + _cubicNeighbors[NormalizeIndex(neighborIndex)]; + } + + #endregion + + #region GetNeighbors + + /// + /// Return all neighbors of the hex + /// + public IEnumerable GetNeighbors(Offset hex) + { + foreach (var offset in GetNeighborsOffsets(hex)) + { + yield return offset + hex; + } + } + + /// + /// Return all neighbors of the hex + /// + public IEnumerable GetNeighbors(Axial hex) + { + foreach (var offset in _axialNeighbors) + { + yield return offset + hex; + } + } + + /// + /// Return all neighbors of the hex + /// + public IEnumerable GetNeighbors(Cubic hex) + { + foreach (var offset in _cubicNeighbors) + { + yield return offset + hex; + } + } + + #endregion + + #region IsNeighbors + + /// + /// Checks whether the two hexes are neighbors or no + /// + public bool IsNeighbors(Offset coord1, Offset coord2) + { + return IsNeighbors(coord1, coord2, GetNeighbor); + } + + /// + /// Checks whether the two hexes are neighbors or no + /// + public bool IsNeighbors(Axial coord1, Axial coord2) + { + Func getNeighbor = GetNeighbor; + return IsNeighbors(coord1, coord2, getNeighbor); + } + + /// + /// Checks whether the two hexes are neighbors or no + /// + public bool IsNeighbors(Cubic coord1, Cubic coord2) + { + return IsNeighbors(coord1, coord2, GetNeighbor); + } + + /// + /// Checks whether the two hexes are neighbors or no + /// + public bool IsNeighbors(T coord1, T coord2, in Func getNeighbor) + where T : struct, IEqualityComparer + { + for (var neighborIndex = 0; neighborIndex < EdgesCount; neighborIndex++) + { + var neighbor = getNeighbor(coord1, neighborIndex); + if (neighbor.Equals(coord2)) + { + return true; + } + } + + return false; + } + + #endregion + + #region GetNeighborsRing + + /// + /// Returns a ring with a radius of hexes around the given . + /// + public IEnumerable GetNeighborsRing(Offset center, int radius) + { + return GetNeighborsRing(center, radius, GetNeighbor); + } + + /// + /// Returns a ring with a radius of hexes around the given . + /// + public IEnumerable GetNeighborsRing(Axial center, int radius) + { + return GetNeighborsRing(center, radius, GetNeighbor); + } + + /// + /// Returns a ring with a radius of hexes around the given . + /// + public IEnumerable GetNeighborsRing(Cubic center, int radius) + { + return GetNeighborsRing(center, radius, GetNeighbor); + } + + /// + /// Returns a ring with a radius of hexes around the given . + /// + private static IEnumerable GetNeighborsRing(T center, int radius, Func getNeighbor) + where T : struct + { + if (radius == 0) + { + yield return center; + yield break; + } + + for (var i = 0; i < radius; i++) + { + center = getNeighbor(center, 4); + } + + for (var i = 0; i < 6; i++) + { + for (var j = 0; j < radius; j++) + { + yield return center; + center = getNeighbor(center, i); + } + } + } + + #endregion + + #region GetNeighborsAround + + /// + /// Returns a all hexes in the ring with a radius of hexes around the given . + /// + public IEnumerable GetNeighborsAround(Offset center, int radius) + { + return GetNeighborsAround(center, radius, GetNeighborsRing); + } + + /// + /// Returns a all hexes in the ring with a radius of hexes around the given . + /// + public IEnumerable GetNeighborsAround(Axial center, int radius) + { + return GetNeighborsAround(center, radius, GetNeighborsRing); + } + + /// + /// Returns a all hexes in the ring with a radius of hexes around the given . + /// + public IEnumerable GetNeighborsAround(Cubic center, int radius) + { + return GetNeighborsAround(center, radius, GetNeighborsRing); + } + + /// + /// Returns a all hexes in the ring with a radius of hexes around the given . + /// + private static IEnumerable GetNeighborsAround(T center, int radius, Func> getNeighborRing) + where T : struct + { + for (var i = 0; i < radius; i++) + { + foreach (var hex in getNeighborRing(center, i)) + { + yield return hex; + } + } + } + + #endregion + + #region GetNeighborIndex + + /// + /// Returns the bypass index to the specified neighbor + /// + public byte GetNeighborIndex(Offset center, Offset neighbor) + { + return GetNeighborIndex(center, neighbor, GetNeighbors); + } + + /// + /// Returns the bypass index to the specified neighbor + /// + public byte GetNeighborIndex(Axial center, Axial neighbor) + { + return GetNeighborIndex(center, neighbor, GetNeighbors); + } + + /// + /// Returns the bypass index to the specified neighbor + /// + public byte GetNeighborIndex(Cubic center, Cubic neighbor) + { + return GetNeighborIndex(center, neighbor, GetNeighbors); + } + + /// + /// Returns the bypass index to the specified neighbor + /// + private byte GetNeighborIndex(T center, T neighbor, Func> getNeighbors) + where T : struct, IEqualityComparer + { + byte neighborIndex = 0; + foreach (var current in getNeighbors(center)) + { + if (current.Equals(neighbor)) + { + return neighborIndex; + } + + neighborIndex++; + } + + throw new HexagonalException($"Can't find bypass index", this, (nameof(center), center), (nameof(neighbor), neighbor)); + } + + #endregion + + #region GetPointBetweenTwoNeighbours + + /// + /// Returns the midpoint of the boundary segment of two neighbors + /// + public (float x, float y) GetPointBetweenTwoNeighbours(Offset coord1, Offset coord2) + { + return GetPointBetweenTwoNeighbours(coord1, coord2, IsNeighbors, ToPoint2); + } + + /// + /// Returns the midpoint of the boundary segment of two neighbors + /// + public (float x, float y) GetPointBetweenTwoNeighbours(Axial coord1, Axial coord2) + { + return GetPointBetweenTwoNeighbours(coord1, coord2, IsNeighbors, ToPoint2); + } + + /// + /// Returns the midpoint of the boundary segment of two neighbors + /// + public (float x, float y) GetPointBetweenTwoNeighbours(Cubic coord1, Cubic coord2) + { + return GetPointBetweenTwoNeighbours(coord1, coord2, IsNeighbors, ToPoint2); + } + + /// + /// Returns the midpoint of the boundary segment of two neighbors + /// + private (float x, float y) GetPointBetweenTwoNeighbours(T coord1, T coord2, Func isNeighbor, Func toPoint) + where T : struct + { + if (!isNeighbor(coord1, coord2)) + { + throw new HexagonalException($"Can't calculate point between not neighbors", this, (nameof(coord1), coord1), (nameof(coord2), coord2)); + } + + var c1 = toPoint(coord1); + var c2 = toPoint(coord2); + + return ((c1.X + c2.X) / 2, (c1.Y + c2.Y) / 2); + } + + #endregion + + #region CubeDistance + + /// + /// Manhattan distance between two hexes + /// + public int CubeDistance(Offset h1, Offset h2) + { + var cubicFrom = ToCubic(h1); + var cubicTo = ToCubic(h2); + return CubeDistance(cubicFrom, cubicTo); + } + + /// + /// Manhattan distance between two hexes + /// + public int CubeDistance(Axial h1, Axial h2) + { + var cubicFrom = ToCubic(h1); + var cubicTo = ToCubic(h2); + return CubeDistance(cubicFrom, cubicTo); + } + + /// + /// Manhattan distance between two hexes + /// + public static int CubeDistance(Cubic h1, Cubic h2) + { + return (Abs(h1.X - h2.X) + Abs(h1.Y - h2.Y) + Abs(h1.Z - h2.Z)) / 2; + } + + #endregion + + #region Neighbors + + /// + /// Return all neighbors offsets of the hex + /// + private IReadOnlyList GetNeighborsOffsets(Offset coord) + { + switch (Type) + { + case HexagonalGridType.PointyOdd: + return Abs(coord.Y % 2) == 0 ? _pointyEvenNeighbors : _pointyOddNeighbors; + case HexagonalGridType.PointyEven: + return Abs(coord.Y % 2) == 1 ? _pointyEvenNeighbors : _pointyOddNeighbors; + case HexagonalGridType.FlatOdd: + return Abs(coord.X % 2) == 0 ? _flatEvenNeighbors : _flatOddNeighbors; + case HexagonalGridType.FlatEven: + return Abs(coord.X % 2) == 1 ? _flatEvenNeighbors : _flatOddNeighbors; + default: + throw new HexagonalException($"{nameof(GetNeighborsOffsets)} failed with unexpected {nameof(Type)}", this, (nameof(coord), coord)); + } + } + + private static readonly List _pointyOddNeighbors = new List + { + new Offset(+1, 0), new Offset(+1, -1), new Offset(0, -1), + new Offset(-1, 0), new Offset(0, +1), new Offset(+1, +1), + }; + + private static readonly List _pointyEvenNeighbors = new List + { + new Offset(+1, 0), new Offset(0, -1), new Offset(-1, -1), + new Offset(-1, 0), new Offset(-1, +1), new Offset(0, +1), + }; + + private static readonly List _flatOddNeighbors = new List + { + new Offset(+1, +1), new Offset(+1, 0), new Offset(0, -1), + new Offset(-1, 0), new Offset(-1, +1), new Offset(0, +1), + }; + + private static readonly List _flatEvenNeighbors = new List + { + new Offset(+1, 0), new Offset(+1, -1), new Offset(0, -1), + new Offset(-1, -1), new Offset(-1, 0), new Offset(0, +1), + }; + + private static readonly List _axialNeighbors = new List + { + new Axial(+1, 0), new Axial(+1, -1), new Axial(0, -1), + new Axial(-1, 0), new Axial(-1, +1), new Axial(0, +1), + }; + + private static readonly List _cubicNeighbors = new List + { + new Cubic(+1, -1, 0), new Cubic(+1, 0, -1), new Cubic(0, +1, -1), + new Cubic(-1, +1, 0), new Cubic(-1, 0, +1), new Cubic(0, -1, +1), + }; + + #endregion + + private static int NormalizeIndex(int index) + { + index = index % EdgesCount; + if (index < 0) + { + index += EdgesCount; + } + + return index; + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalGridMesh.cs b/Src/HexagonalLib/src/HexagonalLib/HexagonalGridMesh.cs new file mode 100644 index 0000000..5334b83 --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalGridMesh.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using HexagonalLib.Coordinates; + +namespace HexagonalLib +{ + public readonly partial struct HexagonalGrid + { + /// + /// Calculate count of vertices and indices needed for build mesh. + /// + /// Count on hexes in mesh + /// Count of triangles splits for each hex + public (int VerticesCount, int IndicesCount) GetMeshData(int hexesCount, int subdivide) + { + // Calculate number of vertices we need to generate + // numVertices = 1 + Σ {S, i = 1} (i * 6) + var numVertices = 1; + + // Calculate number of indices we need to generate + // numIndices = Σ {S, i = 1} (36 * i - 18) + var numIndices = 0; + + for (int i = 1; i <= subdivide; i++) + { + numVertices += i * 6; + numIndices += 36 * i - 18; + } + + return (numVertices * hexesCount, numIndices * hexesCount); + } + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// + public void CreateMesh(IEnumerable hexes, int subdivide, Action setVertex, Action setIndex) + { + CreateMesh(hexes, subdivide, setVertex, setIndex, ToPoint2); + } + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// + public void CreateMesh(IEnumerable hexes, int subdivide, Action setVertex, Action setIndex) + { + CreateMesh(hexes, subdivide, setVertex, setIndex, ToPoint2); + } + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// + public void CreateMesh(IEnumerable hexes, int subdivide, Action setVertex, Action setIndex) + { + CreateMesh(hexes, subdivide, setVertex, setIndex, ToPoint2); + } + + /// + /// Generates a mesh for list of hex. The generation algorithm is taken from the site: + /// http://www.voidinspace.com/2014/07/project-twa-part-1-generating-a-hexagonal-tile-and-its-triangular-grid/ + /// + private void CreateMesh(IEnumerable hexes, int subdivide, Action setVertex, Action setIndex, Func toPoint) + { + var vertex = 0; + var index = 0; + + var data = GetMeshData(1, subdivide); + + foreach (var hex in hexes) + { + var localVertex = vertex; + var localIndex = index; + var center = toPoint(hex); + + void setVertexLocal(int i, (float X, float Y) currentVertex) + { + var (x, y) = currentVertex; + var shifted = (x + center.X, y + center.Y); + setVertex(localVertex + i, shifted); + } + + void setIndexLocal(int i, int currentIndex) => setIndex(localIndex + i, localVertex + currentIndex); + + CreateMesh(subdivide, setVertexLocal, setIndexLocal); + + vertex += data.VerticesCount; + index += data.IndicesCount; + } + } + + /// + /// Generates a mesh for one hex. The generation algorithm is taken from the site: + /// http://www.voidinspace.com/2014/07/project-twa-part-1-generating-a-hexagonal-tile-and-its-triangular-grid/ + /// + public void CreateMesh(int subdivide, Action setVertex, Action setIndex) + { + // We'll need those later to generate our x, z coordinates + var radius = InscribedRadius / (float) Math.Cos(Math.PI / 6); + var sin60 = (float) Math.Sin(Math.PI / 3); + var invTan60 = 1.0f / (float) Math.Tan(Math.PI / 3.0f); + var rdq = radius / subdivide; + + var verticesIndex = 0; + var indicesIndex = 0; + + // We'll need those to calculate our indices + var currentNumPoints = 0; + var prevRowNumPoints = 0; + + // [colMin, colMax] + var npCol0 = 2 * subdivide + 1; + var colMin = -subdivide; + var colMax = subdivide; + + // + // Now let's generate the grid + // First we'll iterate through the Columns, starting from the bottom (fA) + for (var itC = colMin; itC <= colMax; itC++) + { + // Calculate z for this row + // That's the same for each point in this column + var x = sin60 * rdq * itC; + + // Calculate how many points (z values) we need to generate on for this column + var npColI = npCol0 - Math.Abs(itC); + + // [rowMin, rowMax] + var rowMin = -subdivide; + if (itC < 0) + { + rowMin += Math.Abs(itC); + } + + var rowMax = rowMin + npColI - 1; + + // We need this for the indices + currentNumPoints += npColI; + + // Iterate through the Rows (fB) + for (var itR = rowMin; itR <= rowMax; itR++) + { + // Calculate z + var z = invTan60 * x + rdq * itR; + setVertex(verticesIndex, (x, z).Rotate(AngleToFirstNeighbor)); + + // Indices + // From each point we'll try to create triangles left and right + if (verticesIndex < (currentNumPoints - 1)) + { + // Triangles left from this column + if (itC >= colMin && itC < colMax) + { + // To get the point above + var padLeft = 0; + if (itC < 0) + { + padLeft = 1; + } + + setIndex(indicesIndex++, verticesIndex + npColI + padLeft); + setIndex(indicesIndex++, verticesIndex + 1); + setIndex(indicesIndex++, verticesIndex); + } + + // Triangles right from this column + if (itC > colMin && itC <= colMax) + { + // To get point bellow + var padRight = 0; + if (itC > 0) + { + padRight = 1; + } + + setIndex(indicesIndex++, verticesIndex - prevRowNumPoints + padRight); + setIndex(indicesIndex++, verticesIndex); + setIndex(indicesIndex++, verticesIndex + 1); + } + } + + // Next vertex... + verticesIndex++; + } + + // Store the previous row number of points, used to calculate the index buffer + prevRowNumPoints = npColI; + } + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalGridType.cs b/Src/HexagonalLib/src/HexagonalLib/HexagonalGridType.cs new file mode 100644 index 0000000..57b86fb --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalGridType.cs @@ -0,0 +1,28 @@ +namespace HexagonalLib +{ + /// + /// The typical layouts and orientations for hex grids + /// + public enum HexagonalGridType : byte + { + /// + /// Horizontal layout shoves odd rows right [odd-r] + /// + PointyOdd, + + /// + /// Horizontal layout shoves even rows right [even-r] + /// + PointyEven, + + /// + /// Vertical layout shoves odd columns down [odd-q] + /// + FlatOdd, + + /// + /// Vertical layout shoves even columns down [even-q] + /// + FlatEven, + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalLib.projitems b/Src/HexagonalLib/src/HexagonalLib/HexagonalLib.projitems new file mode 100644 index 0000000..1329fbf --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalLib.projitems @@ -0,0 +1,21 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 1FDA753A-7694-41B7-8C00-AA39FD0780CA + + + HexagonalLib + + + + + + + + + + + + \ No newline at end of file diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalLib.shproj b/Src/HexagonalLib/src/HexagonalLib/HexagonalLib.shproj new file mode 100644 index 0000000..e6c65de --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalLib.shproj @@ -0,0 +1,18 @@ + + + + {30905D5D-8D2E-4A25-ABEB-B4B4B0C440E6} + HexagonalLib + + + bin\Debug\ + + + bin\Release\ + + + + + + + diff --git a/Src/HexagonalLib/src/HexagonalLib/HexagonalMath.cs b/Src/HexagonalLib/src/HexagonalLib/HexagonalMath.cs new file mode 100644 index 0000000..629c77c --- /dev/null +++ b/Src/HexagonalLib/src/HexagonalLib/HexagonalMath.cs @@ -0,0 +1,47 @@ +using System; + +namespace HexagonalLib +{ + public static partial class HexagonalMath + { + /// + /// Rotate 2d vector around z Axis clockwise + /// + /// Vector to rotate + /// Angle of rotation + public static (float X, float Y) Rotate(this in (float X, float Y) vector, float degrees) + { + var radians = Math.PI / 180.0 * degrees; + var sin = Math.Sin(radians); + var cos = Math.Cos(radians); + var x = (float) Math.Round(cos * vector.X - sin * vector.Y, 6); + var y = (float) Math.Round(sin * vector.X + cos * vector.Y, 6); + return (-x, y); + } + + /// + /// Returns this vector with a magnitude of 1 + /// + public static (float X, float Y) Normalize(this in (float X, float Y) vector) + { + var distance = Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y); + return ((float) (vector.X / distance), (float) (vector.Y / distance)); + } + + /// + /// Compares two floating point values and returns true if they are similar. + /// + public static bool SimilarTo(this in float a, in float b) + { + return Math.Abs(b - a) < (double) Math.Max(1E-06f * Math.Max(Math.Abs(a), Math.Abs(b)), float.Epsilon * 8f); + } + + /// + /// Compares two vectors and returns true if they are similar. + /// + public static bool SimilarTo(this in (float X, float Y) a, in (float X, float Y) b) + { + return a.X.SimilarTo(b.X) && a.Y.SimilarTo(b.Y); + } + } +} \ No newline at end of file diff --git a/Src/HexagonalLib/src/nuget.config b/Src/HexagonalLib/src/nuget.config new file mode 100644 index 0000000..4258823 --- /dev/null +++ b/Src/HexagonalLib/src/nuget.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Tests/Giants.Core.Tests/ApplicationTests.cs b/Tests/Giants.Core.Tests/ApplicationTests.cs new file mode 100644 index 0000000..3c97a2e --- /dev/null +++ b/Tests/Giants.Core.Tests/ApplicationTests.cs @@ -0,0 +1,12 @@ +namespace Giants.Core.Tests; + +using Giants.Application; + +public class ApplicationTests +{ + [Fact] + public void ApplicationCreationExample() + { + GiantApplication app = new GiantApplication(); + } +} diff --git a/Tests/Giants.Core.Tests/Giants.Core.Tests.csproj b/Tests/Giants.Core.Tests/Giants.Core.Tests.csproj new file mode 100644 index 0000000..0916eb4 --- /dev/null +++ b/Tests/Giants.Core.Tests/Giants.Core.Tests.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + +