Hexagonly
Provides helper classes for performing flat-topped hexagonal tiling and other polygon-related operations.
Features
- Currently supported shapes: Point, Polygon, Hexagon.
- Define a Polygon and check whether a Point lies within its boundries (crossing count algorithm).
- Define a Hexagon and determine its boundries, based on the hexagon center and size. All Polygon methods apply to this shape as well.
- Generate neighbouring Hexagons for a given Hexagon.
- Hexagonal tiling: generate hexagons to fill up a space, based on its boundries (two points suffice) and the hexagon size.
- Hexagonal tiling & collecting objects on the way: generate hexagons to match the boundries of a given collection of Points (a space), then store contained Points (or custom objects) for every Hexagon.
- Convert shapes (Polygon, Hexagon, Point) and mixed collections of shapes to GeoJson.
- For every defined shape you can either use pre-defined classes or use your own custom classes, by including the appropriate Hexagonly shape module.
- RSpec test suite.
The gem currently supports flat-topped hexagons only. For pointy-topped hexagons just place a bug request and I'll look into it.
Installation
Add this line to your application's Gemfile:
gem 'hexagonly'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install hexagonly
Usage
Points
There are 2 ways for defining Point objects:
- By using the pre-defined
Hexagonly::Point
class:
point = Hexagonly::Point.new(1, 2)
puts point.x_coord # => 1
puts point.y_coord # => 2
- By using your custom class (e.g. think ActiveRecord) and including
Hexagonly::Point::Methods
inside your class definition. Then you would assign your own accessors as coordinate getters and setters. This is accomplished via the class methodx_y_coord_methods
which takes two arguments: the names of the x and y coordinate accessors. Thex_y_coord_methods
method defaults to:x
and:y
.
class MyCustomPoint
include Hexagonly::Point::Methods
# Sets accessors :a and :b as coordinate accessors
x_y_coord_methods :a, :b
attr_accessor :a, :b
def initialize(a, b)
@a, @b = a, b
end
end
point = MyCustomPoint.new(1, 2)
puts point.x_coord # => 1
puts point.y_coord # => 2
Poylgons
The same 2 ways of instanciating apply to Polygons as well:
-
By using the pre-defined
Hexagonly::Polygon
class:corners = [ Hexagonly::Point.new(2, 1), Hexagonly::Point.new(5, 5), Hexagonly::Point.new(6, 1), ... ] poly = Hexagonly::Polygon(corners) puts poly.poly_points # => corners... puts poly.contains?(Hexagonly::Point(4, 2)) # => true
-
By using custom classes, which include
Hexagonly::Polygon::Methods
and assigning custom corner accessor, set via the class methodpoly_points_method
. Thepoly_points_method
method defaults to:poly_points
.
class MyCustomPolygon
include Hexagonly::Polygon::Methods
# Sets :corners as the polygon corners accessor
poly_points_method :corners
attr_accessor :corners
def initialize(corners)
@corners = corners
end
end
corners = [ Hexagonly::Point.new(2, 1), Hexagonly::Point.new(5, 5), Hexagonly::Point.new(6, 1), ... ]
poly = MyCustomPolygon.new(corners)
puts poly.poly_points # => corners...
puts poly.contains?(Hexagonly::Point(4, 2)) # => true
Hexagons
Hexagons inherit all methods from Polygons. There are 2 ways of creating new Hexagons:
- By using the pre-defined
Hexagonly::Hexagon
class:
center = Hexagonly::Point.new(4, 4)
hexagon = Hexagonly::Hexagon.new(center, 1.5)
puts hexagon.hex_corners # => corners...
puts hexagon.contains?(center) # => true
- By using a custom class, which includes
Hexagonly::Hexagon::Methods
, and callingsetup_hex
on your instance:
class MyCustomHexagon
include Hexagonly::Hexagon::Methods
def initialize(center, size)
setup_hex(center, size)
end
end
center = Hexagonly::Point.new(4, 4)
hexagon = MyCustomHexagon.new(center, 1.5)
puts hexagon.hex_center # => center
puts hexagon.hex_v_size # => distance from center to the top / bottom borders
puts hexagon.hex_corners # => corners...
puts hexagon.contains?(center) # => true
Hexagonal tiling
You start by defining your boundries. Boundries are basically a collection of 2 or more Hexagonly::Point
objects or objects including Hexagonly::Point::Methods
:
# Use the default Point class
boundries = [
Hexagonly::Point.new(1, 2),
Hexagonly::Point.new(4, 5),
...
]
# Or use a custom class that includes Hexagonly::Point::Methods
boundries = [
MyCustomPoint.new(1, 2),
MyCustomPoint.new(4, 5),
...
]
Once you've defined your boundries, you can pass them to the Hexagonly::Hexagon.pack
method. This takes 3 arguments:
- The boundries or points that mark your hexagon field.
- The distance from the center to the left / right boundries (half of the desired width of your hexagons).
- A Hash of additional parameters:
-
:hexagon_class
: The class used to instanciate new Hexagons. Defaults toHexagonly::Hexagon
. If you are using custom Hexagon classes, you should include your class here. -
:point_class
: The class used to instanciate Hexagon center points. Defaults toHexagonly::Point
. -
:grab_points
: A boolean, determining whether the first argument (points / boundries) will also be used to collect contained points for every generated hexagon (see next category). -
:reject_empty
: A boolean, determining whether generated hexagons with no collected points should be removed from the result. Only works if:grab_points
is enabled (see next category).
-
# Generated Hexagons will be Hexagonly::Hexagon instances
hexagons = Hexagonly::Hexagon.pack(boundries, 0.3)
# Generated Hexagons will be MyCustomHexagon instances
hexagons = Hexagonly::Hexagon.pack(boundries, 0.3, { :hexagon_class => MyCustomHexagon })
Hexagonal tiling & collecting objects on the way
While generating your hexagons from a collection of Point objects, you might want to store all contained points within individual hexagons.
This is accomplished by the same Hexagonly::Hexagon.pack
method, only you'll need to enable the additional :grab_points
parameter.
Enabling the:reject_empty
parameter will remove all empty hexagons from the results Array.
points = [
Hexagonly::Point.new(1, 1),
Hexagonly::Point.new(2, 2),
Hexagonly::Point.new(2, 3),
Hexagonly::Point.new(7, 1),
...
]
hexagons = Hexagonly::Hexagon.pack(points, 0.25, { :grab_points => true, :reject_empty => true })
puts hexagons[0].collected_points # => all objects from the points variable contained within this hexagon
puts hexagons[0].collected_points[0].class # => Hexagonly::Point or your custom point class
Examples with geographical coordinates
-
This one applies hexagonal tiling & grabbing over a collection of 100 points. The
:reject_empty
option is disabled. -
This example uses the same collection, but the
:reject_empty
option has been enabled.
More examples
While tiling and grabbing objects, you can also mix classes in your points
collection, as long as they are
compatible with Hexagonly::Point
methods:
class Salami
include Hexagonly::Point::Methods
# .x_y_coord_methods defaults to :x and :y
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
end
class Cheese
include Hexagonly::Point::Methods
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
end
class Pizza
include Hexagonly::Hexagon::Methods
end
# We have a couple of ingredients layed out on the table
ingredients = [
Salami.new(1, 1),
Cheese.new(2, 2),
Cheese.new(3, 3),
Salami.new(4, 4),
...
]
# And we want to create hexagonal pizzas out of them,
# by just laying the dough on top and removing the extra dough
pizza_size = 1.0
pizzas = Hexagonly::Hexagon.pack(ingredients, pizza_size, {
:hexagon_class => Pizza,
:grab_points => true,
:reject_empty => true }
)
puts pizzas[0].class # => Pizza
puts pizzas[0].collected_points[0].class # => Salami or Cheese
Motivation
A personal project required me to group geographical coordinates on a map, compute different stats for individual groups and display those stats in such a way that made them visually identify their groups. Squares and rectangles didn't really work for me, because different points on the boundries aren't equally distanced to the center. Circles would have been a good option for grouping objects in a 2-dimensional space, but then again circles are not really tileable. Hexagons, on the other hand, combine properties of the two shapes: they are easily tileable and have (sort of) a radius.
I've tested this on my project with > 5000 points, but I guess it should work with 2D games as well, or anything else that requires tiling or polygon-related operations.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request