diff --git a/demo/demo/hexboard/board.cpp b/demo/demo/hexboard/board.cpp
index 174826585af85f9670d3f603be0fc5fac8928b50..9c12a820cfd966da6195c66402336e54be4d7225 100644
--- a/demo/demo/hexboard/board.cpp
+++ b/demo/demo/hexboard/board.cpp
@@ -25,10 +25,27 @@ namespace demo::hexboard {
 
 	void Scene::activate() {
 		m_board = std::make_unique<fggl::grid::HexGrid>();
+		m_layout = std::make_unique<fggl::grid::Layout>( fggl::grid::Orientation::make_pointy(), 64.0F );
+		m_selections = std::make_unique<SelectionModel>();
+
+		fggl::grid::TerrainType grass{
+			.data = std::make_shared<fggl::grid::MaterialData>()
+
+		};
+		grass.data->name = "grass";
+		grass.data->colour = {0.0F, 1.0F, 0.0};
+
+		fggl::grid::IntHex islandPoint{3,3};
+		auto island = islandPoint.hexesInRange(2);
+		for ( auto& hex : island) {
+			m_board->setTerrain(hex, grass);
+		}
+
 	}
 
 	void Scene::deactivate() {
 		m_board = nullptr;
+		m_selections = nullptr;
 	}
 
 	void Scene::update(float delta) {
@@ -38,45 +55,40 @@ namespace demo::hexboard {
 		if ( m_board == nullptr ){
 			return;
 		}
-	}
-
-	// see https://www.redblobgames.com/grids/hexagons/#hex-to-pixel
-	const fggl::math::mat2 HEX_BASIS{
-		std::sqrt(3.0F), std::sqrt(3.0F) / 2.0F,
-		0.0F, 3.0F / 2.0F
-	};
-
-	static inline fggl::math::vec2 hexToScreen(fggl::grid::IntHex hexPos, float size, fggl::math::vec2 offset = {0,0}) {
-		return size * fggl::math::vec2{hexPos.q(), hexPos.r()} * HEX_BASIS + offset;
-	}
 
-	void Scene::render(fggl::gfx::Graphics &gfx) {
-		// if the board is not set, abort
-		if ( m_board == nullptr ){
-			return;
+		// check if a button was pressed
+		auto& input = this->input();
+		{
+			fggl::math::vec2 screenPos(
+				input.mouse.axis(fggl::input::MouseAxis::X),
+				input.mouse.axis(fggl::input::MouseAxis::Y)
+			);
+			screenPos.x = fggl::math::rescale_ndc(screenPos.x, 0, 1920);
+			screenPos.y = fggl::math::rescale_ndc(screenPos.y, 0, 1080);
+			m_selections->hover = fggl::grid::round2( m_layout->toGrid(screenPos) );
+
+			if (input.mouse.pressed(fggl::input::MouseButton::LEFT)) {
+				m_selections->selected = m_selections->hover;
+			}
 		}
+	}
 
-		// draw the grid
-		// FIXME don't hard-code the screen size
+	void Scene::drawGrid(fggl::gfx::Paint& paint) {
 		const float hexRadius = 64.0F;
 		const auto gridWidth = (int)( (1920 - hexRadius) / (hexRadius * std::sqrt(3.0F)) );
 		const auto gridHeight = (int)( (1080 - hexRadius) / (hexRadius * (3.0F / 2.0F) ));
 
-		const fggl::math::vec2 offset{
-			( 1920 - ( (float)gridWidth * hexRadius * std::sqrt(3.0F))) - (hexRadius / 2.0F),
-			( (1080 - hexRadius / 2.0F) - ( (float)gridHeight * hexRadius * (3.0F / 2.0F) )),
-		};
+		auto tiles = m_board->getAllTiles();
 
-		fggl::gfx::Paint paint;
 		fggl::grid::IntHex hexPos{0, 0};
 		auto rowBasis = hexPos;
 		for(auto i=0; i<gridHeight; ++i) {
 			for (auto j=0; j<gridWidth; ++j) {
 
-				if ( m_board->isValidPos(hexPos) ) {
-					auto pos = hexToScreen(hexPos, hexRadius, offset);
-					auto hexShape = fggl::gfx::make_shape(pos, hexRadius, 6);
-					paint.stroke(hexShape);
+				auto terrain = m_board->getTerrain(hexPos);
+				if ( terrain.has_value() ) {
+					const auto& value = terrain.value();
+					m_layout->paintHex(paint, hexPos, value.colour);
 				}
 
 				// next hexagon
@@ -86,7 +98,33 @@ namespace demo::hexboard {
 			rowBasis = i % 2 == 0 ? rowBasis.neighbour(fggl::grid::HexDirPointy::BOTTOM_RIGHT) : rowBasis.neighbour(fggl::grid::HexDirPointy::BOTTOM_LEFT);
 			hexPos = rowBasis;
 		}
+	}
+
+	void Scene::drawSelections(fggl::gfx::Paint& paint) {
+		if ( m_selections == nullptr ) {
+			return;
+		}
+
+		if ( m_selections->selected.has_value() ) {
+			m_layout->paintHex( paint, m_selections->selected.value(), fggl::gfx::colours::YELLOW);
+		}
+
+		if ( m_selections->hover.has_value() ) {
+			m_layout->paintHex( paint, m_selections->hover.value(), fggl::gfx::colours::BLANCHED_ALMOND);
+		}
+	}
 
+	void Scene::render(fggl::gfx::Graphics &gfx) {
+		// if the board is not set, abort
+		if ( m_board == nullptr ){
+			return;
+		}
+
+		// draw the grid
+		// FIXME don't hard-code the screen size
+		fggl::gfx::Paint paint;
+		drawGrid(paint);
+		drawSelections(paint);
 		gfx.draw2D(paint);
 	}
 
diff --git a/demo/include/hexboard/scene.hpp b/demo/include/hexboard/scene.hpp
index 0ded844c82ab02d80ce86b1dcef5f577a7c388e8..0fe38cd88783f63e763f588fa8d7f33505bf79a3 100644
--- a/demo/include/hexboard/scene.hpp
+++ b/demo/include/hexboard/scene.hpp
@@ -20,12 +20,19 @@
 #define FGGL_DEMO_INCLUDE_HEXBOARD_SCENE_H
 
 #include <memory>
+#include <optional>
 
 #include "fggl/scenes/game.hpp"
 #include "fggl/grid/hexgrid.hpp"
+#include "fggl/grid/layout.hpp"
 
 namespace demo::hexboard {
 
+	struct SelectionModel {
+		std::optional<fggl::grid::IntHex> selected;
+		std::optional<fggl::grid::IntHex> hover;
+	};
+
 	class Scene : public fggl::scenes::GameBase {
 		public:
 			explicit Scene(fggl::App& app);
@@ -38,6 +45,11 @@ namespace demo::hexboard {
 
 		private:
 			std::unique_ptr<fggl::grid::HexGrid> m_board;
+			std::unique_ptr<fggl::grid::Layout> m_layout;
+			std::unique_ptr<SelectionModel> m_selections;
+
+			void drawGrid(fggl::gfx::Paint&);
+			void drawSelections(fggl::gfx::Paint&);
 	};
 
 } // namespace demo::hexboard
diff --git a/fggl/grid/hexagon.cpp b/fggl/grid/hexagon.cpp
index 25a5d30f7b2d9a71c8ded733998c15aa416cd408..b07b598167f794aa34c3d611f7bde599a58a8baa 100644
--- a/fggl/grid/hexagon.cpp
+++ b/fggl/grid/hexagon.cpp
@@ -24,7 +24,7 @@ namespace fggl::grid {
 		int distance = start.distance(end);
 		std::vector<IntHex> line;
 		for (auto i=0; i < distance; ++i) {
-			line.push_back( round(hexLerp(start, end, 1.0F/distance * i)) );
+			line.push_back( round2(hexLerp(start, end, 1.0F/distance * i)) );
 		}
 		return line;
 	}
diff --git a/include/fggl/gfx/paint.hpp b/include/fggl/gfx/paint.hpp
index 6ffaad3ce4e2b529c7afd28a18bb861059f5e6e4..884cc498114dc0844011bfc03901f564af084120 100644
--- a/include/fggl/gfx/paint.hpp
+++ b/include/fggl/gfx/paint.hpp
@@ -225,21 +225,22 @@ namespace fggl::gfx {
 	};
 
 	inline Path2D make_shape(math::vec2 center, float radius, int sides, math::vec3 colour = colours::WHITE, ShapeOpts opts = {}) {
-		float angle = (math::PI * 2.0F) / sides;
+		float angle = ((math::PI * 2.0F) / sides);
 
 		fggl::gfx::Path2D tileGfx(center);
 		tileGfx.colour(colour);
 
 		for (int i=0; i < sides; ++i) {
-			float xPos = (float)(sinf(i * angle + opts.angleOffset) * radius) + center.x;
-			float yPos = (float)(cosf(i * angle + opts.angleOffset) * radius) + center.y;
-			if (!opts.sinFirst) {
-				std::swap(xPos, yPos);
-			}
+			math::vec2 pos (
+				(float)(cosf(i * angle + opts.angleOffset) * radius),
+				(float)(sinf(i * angle + opts.angleOffset) * radius)
+				);
+			pos += center;
+
 			if ( i == 0 ) {
-				tileGfx.moveTo( {xPos, yPos} );
+				tileGfx.moveTo( pos );
 			} else {
-				tileGfx.pathTo({xPos, yPos});
+				tileGfx.pathTo( pos );
 			}
 		}
 		tileGfx.close();
diff --git a/include/fggl/grid/hexagon.hpp b/include/fggl/grid/hexagon.hpp
index b5bb3b14f84359644f9244bf47e1dbd57ed04f7b..7277d65d23eb70c293d9b1b4b0e7bdfb716f92af 100644
--- a/include/fggl/grid/hexagon.hpp
+++ b/include/fggl/grid/hexagon.hpp
@@ -111,7 +111,7 @@ namespace fggl::grid {
 			for ( auto q = -range; q <= range; ++q ) {
 				auto stopCount = std::min(range, -q+range);
 				for ( auto r = std::max(-range, -q-range); r <= stopCount; ++r ) {
-					results.push_back( this + HexPointT<T>(q, r) );
+					results.push_back( *this + HexPointT<T>(q, r) );
 				}
 			}
 			return results;
@@ -132,15 +132,25 @@ namespace fggl::grid {
 		};
 	}
 
-	constexpr IntHex round(const FloatHex& hex) {
-		// see https://observablehq.com/@jrus/hexround for original JS implementation
-		float xGrid = std::round( hex.r() );
-		float yGrid = std::round( hex.q() );
-		float x = hex.q() - xGrid;
-		float y = hex.r() - yGrid;
-		auto dx = std::round(x + 0.5F*y) * (float)(x*x >= y*y);
-		auto dy = std::round(y + 0.5F*x) * (float)(x*x < y*y);
-		return { (int)(xGrid + dx), (int)(yGrid + dy) };
+	[[nodiscard]]
+	constexpr IntHex round2(const FloatHex& hex) {
+		auto q = std::round( hex.q() );
+		auto r = std::round( hex.r() );
+		auto s = std::round( hex.s() );
+
+		auto qDiff = std::abs( q - hex.q() );
+		auto rDiff = std::abs( r - hex.r() );
+		auto sDiff = std::abs( s - hex.r() );
+
+		if ( qDiff > rDiff && qDiff > sDiff) {
+			q = -r-s;
+		} else if ( rDiff > sDiff ) {
+			r = -q-s;
+		} else {
+			s = -q-r;
+		}
+
+		return {(int)q, (int)r};
 	}
 
 	std::vector<IntHex> lineTo(const IntHex& start, const IntHex& end);
diff --git a/include/fggl/grid/hexgrid.hpp b/include/fggl/grid/hexgrid.hpp
index ae948db54667985a4f172438950620ef119117c5..374641c86dbb137abf468814a1a0db41d5fdf476 100644
--- a/include/fggl/grid/hexgrid.hpp
+++ b/include/fggl/grid/hexgrid.hpp
@@ -19,15 +19,69 @@
 #ifndef FGGL_GRID_HEXAGON_BOARD_HPP
 #define FGGL_GRID_HEXAGON_BOARD_HPP
 
+#include <map>
+#include <set>
+#include <optional>
+
 #include "fggl/grid/hexagon.hpp"
+#include "fggl/math/types.hpp"
 
 namespace fggl::grid {
 
+	struct MaterialData {
+		std::string name;
+		math::vec3 colour;
+	};
+
+	struct TerrainType {
+		std::shared_ptr<MaterialData> data;
+	};
+
+	struct HexTile {
+		std::shared_ptr<MaterialData> terrain;
+
+		[[nodiscard]]
+		inline std::optional<const MaterialData> data() const {
+			if (terrain == nullptr) {
+				{}
+			}
+
+			return *terrain.get();
+		}
+	};
+
 	class HexGrid {
 		public:
-			inline bool isValidPos(const IntHex& pos) {
-				return true;
+			inline bool isValidPos(const IntHex& pos) const {
+				return m_tiles.contains(pos);
+			}
+
+			void setTerrain(const IntHex& pos, const TerrainType& terrain) {
+				auto& mapTile = m_tiles[pos];
+				mapTile.terrain = terrain.data;
+			}
+
+			std::optional<const MaterialData> getTerrain(const IntHex& pos) const {
+				const auto itr = m_tiles.find(pos);
+				if ( itr == m_tiles.end() ) {
+					return {};
+				}
+				return itr->second.data();
 			}
+
+			std::set<IntHex> getAllTiles() {
+				std::set<IntHex> posSet;
+				for ( auto& [pos,data] : m_tiles ) {
+					posSet.emplace( pos );
+				}
+				return posSet;
+			}
+
+			std::set<HexTile> tilesInRange(const IntHex& pos, int range) const;
+			std::set<HexTile> neighboursOf(const IntHex& pos) const;
+
+		private:
+			std::map<IntHex, HexTile> m_tiles;
 	};
 
 } // namespace fggl::grid
diff --git a/include/fggl/grid/layout.hpp b/include/fggl/grid/layout.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..96cfa7e15116c633a56965be08db378c2f7fa38b
--- /dev/null
+++ b/include/fggl/grid/layout.hpp
@@ -0,0 +1,132 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 18/12/22.
+//
+
+#ifndef FGGL_FGGL_GRID_LAYOUT_HPP
+#define FGGL_FGGL_GRID_LAYOUT_HPP
+
+#include "fggl/math/types.hpp"
+#include "fggl/grid/hexagon.hpp"
+
+namespace fggl::grid {
+
+	const math::mat2 MAT_HEX_POINTY {
+		std::sqrt(3.0F), 0.0F,
+		std::sqrt(3.0F) / 2.0F, 3.0F/2.0F
+	};
+
+	const math::mat2 MAT_HEX_FLAT {
+		3.F/2.F, std::sqrt(3.F)/2.F,
+		0.0F, std::sqrt(3.F)
+	};
+
+	struct Orientation {
+		math::mat2 m_forward;
+		math::mat2 m_backward;
+		int m_angle;
+
+		inline Orientation(int angle, math::mat2 forward) : m_forward(forward), m_backward(glm::inverse(forward)), m_angle(angle) {};
+
+		static inline Orientation make_pointy() {
+			return { 30, MAT_HEX_POINTY};
+		}
+
+		static inline Orientation make_flat() {
+			return { 0, MAT_HEX_FLAT };
+		}
+	};
+
+
+	struct Layout {
+		Orientation m_orientation;
+		math::vec2 m_size;
+		math::vec2 m_origin;
+
+		Layout(Orientation orientation, math::vec2 size, math::vec2 origin) : m_orientation(orientation), m_size(size), m_origin(origin){}
+		Layout(Orientation orientation, float size) : m_orientation(orientation), m_size(size, size), m_origin() {}
+
+		[[nodiscard]]
+		inline math::vec2 origin() const {
+			return m_origin;
+		}
+
+		[[nodiscard]]
+		inline math::vec2 size() const {
+			return m_size;
+		}
+
+		[[nodiscard]]
+		inline math::vec2 toScreen(IntHex gridPos) const {
+			math::vec2 hexPoint{gridPos.q(), gridPos.r()};
+			auto p = (m_orientation.m_forward * hexPoint) * m_size;
+			return p + m_origin;
+		}
+
+		[[nodiscard]]
+		inline FloatHex toGrid(math::vec2 screen) const {
+			auto point = (screen - m_origin) / m_size;
+			auto p = m_orientation.m_backward * point;
+			return {p.x, p.y};
+		}
+
+		[[nodiscard]]
+		inline math::vec2 cornerOffset(int corner) const {
+			int angInc = (360 / 6) * corner;
+			float angle = (angInc + m_orientation.m_angle) * ( fggl::math::PI / 180.0F);
+			return {
+				m_size.x * cosf(angle),
+				m_size.y * sinf(angle)
+			};
+		}
+
+		void paintHex(fggl::gfx::Paint& paint, IntHex pos, math::vec3 colour) const {
+			const auto hexScreenCenter = toScreen(pos);
+
+			gfx::Path2D path({0,0});
+			path.colour(colour);
+
+			for (int i=0; i < 6; ++i) {
+				auto cornerPos =  hexScreenCenter + cornerOffset(i);
+				if ( i == 0) {
+					path.moveTo(cornerPos);
+				} else {
+					path.pathTo(cornerPos);
+				}
+			}
+			paint.stroke(path);
+		}
+
+
+	};
+
+}
+
+#endif //FGGL_FGGL_GRID_LAYOUT_HPP