honeycomb-grid

repository·master·Indexed 20 days ago

https://github.com/flauwekeul/honeycomb

A TypeScript-based hexagon grid library for modern browsers and Node.js (version 16+). It provides tools to define hex dimensions, manage grids of various shapes using traversers, and handle multiple coordinate systems including Cube, Offset, and Tuple coordinates. The library includes the Grid and Hex classes for managing collections of hexagonal cells and calculating distances, transformations, and spatial properties.

Tokens
27.6K
Snippets
110
Records
141
Agent score
67%

What's inside honeycomb-grid

  1. Overview of Honeycomb

    master

    Honeycomb is a TypeScript-based hexagon grid library designed for flexibility. It is renderer-agnostic, meaning it provides the mathematical and structural logic for hexagonal grids without forcing a specific rendering engine, allowing you to use SVG, Canvas, or other technologies.

    Key capabilities include:

    • Cross-platform support: Works in modern browsers and Node.js (version 16 or higher).
    • Grid Traversal: Supports traversing grids in various shapes such as rectangles, rings, lines, and spirals.
    • Extensibility: Allows for creating custom hexes by extending the built-in Hex and/or Grid classes.
  2. What is a traverser and how to use it

    master

    A Traverser is a function used to iterate over a specific subset of hexes in a Grid in a specific order. While a grid's default iteration order follows the order in which hexes were added, a traverser allows for spatial patterns (like spirals, lines, or rectangles).

    To use a traverser, pass it to the grid.traverse() method. The method internally calls the traverser and returns only the hexes that are actually present in the grid.

    A Traverser follows this type signature:

    type Traverser = (
      createHex: (coordinates?: HexCoordinates) => Hex,
      cursor?: HexCoordinates,
    ) => Iterable<Hex>
    const Hex = defineHex({ dimensions: 30 })
    const grid = new Grid(Hex, rectangle({ width: 5, height: 5 }))
    const spiralTraverser = spiral({ start: [0, 2], radius: 1 })
    
    // Iterates over hexes in a spiral pattern within the grid
    grid.traverse(spiralTraverser)
  3. Combine multiple traversers for complex patterns

    master

    Many methods that accept a single Traverser also accept an array of traversers. This allows you to compose complex paths by chaining simpler ones together.

    When using an array of traversers:

    1. Sequential Execution: The traversers are executed in order.
    2. Implicit Continuity: If a traverser (like line()) does not specify a start coordinate, it will automatically start from where the previous traverser in the array left off.
    3. Default Start: If the first traverser in the array does not specify a start, it defaults to [0, 0].
    const grid = new Grid(Hex, rectangle({ width: 5, height: 5 }))
    
    // Creates a square outline by chaining lines
    const squareOutlineTraverser = [
      line({ direction: Direction.E, length: 4 }), // Starts at [0,0]
      line({ direction: Direction.S, length: 3 }), // Starts where previous ended
      line({ direction: Direction.W, length: 3 }),
      line({ direction: Direction.N, length: 3 }),
    ]
    
    grid.traverse(squareOutlineTraverser)
  4. Avoid extending the Hex constructor for custom properties

    master

    When creating custom hex classes, do not attempt to override the Hex constructor to accept additional arguments. The Grid class expects a constructor signature of new (coordinates?: HexCoordinates) => T. If you add extra parameters to the constructor, they will be ignored when the Grid attempts to instantiate hexes, and extending the coordinates argument with custom properties will also fail.

    // ❌ INCORRECT: Adding extra arguments to the constructor
    class CustomHex extends defineHex({ dimensions: 30, origin: 'topLeft' }) {
      custom: string
    
      constructor(coordinates: HexCoordinates, custom: string) {
        super(coordinates)
        this.custom = custom
      }
    }
    
    const grid = new Grid(CustomHex)
    // TypeScript/Runtime error: Grid only passes coordinates to the constructor
  5. Understand the four types of Hexagonal coordinates

    master

    Honeycomb supports four coordinate systems for representing hexagonal positions. While the library uses axial or cube coordinates internally, you can work with any of these types via the HexCoordinates union type:

    1. Cube: The most explicit type. It uses three coordinates { q, r, s } which must always add up to 0.
    2. Axial: A simplified version of cube coordinates using only { q, r }. The s coordinate is redundant.
    3. Offset: Uses { col, row } coordinates. The specific mapping depends on the hex's offset setting (e.g., even-q, odd-r).
    4. Tuple: A terse representation using arrays, such as [q, r] or [q, r, s].

    Important Note on Hex Properties: When using a Hex instance, you can access all coordinate types as properties. However, only cube coordinates are settable. Offset coordinates are read-only and will throw a TypeError if you attempt to modify them directly.

    const hex = new Hex([1, 2])
    
    // Accessing properties
    hex.q   // 1 (Cube)
    hex.r   // 2 (Cube)
    hex.s   // -3 (Cube)
    hex.col // 2 (Offset)
    hex.row // 2 (Offset)
    
    // Setting coordinates
    hex.q = 2 // Allowed (but ensure q + r + s === 0)
    hex.col = 2 // ❌ TypeError: Offset coordinates are read-only
  6. What a Traverser is and how to implement one

    master

    A Traverser is a function used to iterate over hexes in a grid. It is designed to be composable, meaning it can receive a cursor from a previous traverser to allow for continuous traversal across combined patterns.

    To implement a Traverser, your function must follow this signature:

    1. Accept a createHex function (a factory that produces Hex instances).
    2. Accept an optional cursor (the HexCoordinates where the previous traverser left off).
    3. Return an Iterable<Hex> (such as an Array or a Generator).

    When creating custom traversers, use generics <T extends Hex> to allow your traverser to return specialized hex subtypes.

    type Traverser = (
      // hex factory: a function that creates a hex
      createHex: (coordinates?: HexCoordinates) => Hex,
      // cursor: so that the next traverser knows where to continue traversing
      cursor?: HexCoordinates,
    ) => Iterable<Hex>
  7. Understand HexCoordinates types

    master

    In Honeycomb, HexCoordinates is a union type that allows you to represent a hexagon's position using several different coordinate systems. You can use any of the following formats:

    1. PartialCubeCoordinates: An object with at least two of the three cube components (q, r, s). The third component is inferred.
    2. OffsetCoordinates: Coordinates using an offset system (e.g., for grid-based layouts).
    3. TupleCoordinates: A simple array/tuple format [q, r, s?].

    This flexibility allows you to work with different mathematical models (like Cube for calculations or Offset for rendering) interchangeably within the API.

    // Example of the different shapes HexCoordinates can take:
    const cube: PartialCubeCoordinates = { q: 1, r: -1, s: 0 };
    const tuple: TupleCoordinates = [1, -1, 0];
  8. Understand the Grid interfaces: HexStore, HexIterable, and HexTraversable

    master

    The Grid class in Honeycomb is built upon three primary interfaces that define how hexes are managed and accessed. You can implement these interfaces directly to create custom grid behaviors:

    1. HexStore: Provides basic storage capabilities, including getting and setting hexes.
    2. HexIterable (extends HexStore): Adds the ability to iterate over hexes using methods similar to standard JavaScript Array methods.
    3. HexTraversable (extends HexIterable): Adds capabilities to create and traverse hexes.

    By implementing these interfaces, you can integrate your own data structures or external storage systems with Honeycomb's API.

  9. Render hexes using SVG.js

    master

    Honeycomb does not include a built-in renderer. To render a grid using SVG.js, iterate over the Grid instance and use the hex.corners property to generate SVG polygon points.

    Note that by default, hex coordinates are centered. If you prefer the origin to be the top-left corner of the hex's bounding box, set origin: 'topLeft' in the defineHex configuration.

    import { SVG } from '@svgdotjs/svg.js'
    import { defineHex, Grid, rectangle } from 'honeycomb-grid'
    
    // Set origin to 'topLeft' if you want coordinates relative to the bounding box
    const Hex = defineHex({ dimensions: 30, origin: 'topLeft' })
    const grid = new Grid(Hex, rectangle({ width: 10, height: 10 }))
    
    const draw = SVG().addTo('body').size('100%', '100%')
    
    grid.forEach(renderSVG)
    
    function renderSVG(hex: Hex) {
      const polygon = draw
        // create a polygon from a hex's corner points
        .polygon(hex.corners.map(({ x, y }) => `${x},${y}`))
        .fill('none')
        .stroke({ width: 1, color: '#999' })
    
      return draw.group().add(polygon)
    }
  10. Add custom properties and methods to hexes

    master

    Since defineHex() returns a standard JavaScript class, you can extend it to add custom logic or data.

    Best Practices

    • Methods and Getters/Setters: Define these directly on the class. They exist on the prototype and are shared across all hex instances, which is performant.
    • Instance Properties: Use these for data unique to a specific hex instance.

    TypeScript Tip

    If using strict mode, you may need to use a definite assignment assertion (!) or make properties optional (?) to satisfy the compiler when properties are initialized via custom logic rather than a constructor.

    class CustomHex extends defineHex({ dimensions: 30, origin: 'topLeft' }) {
      get prototypeProp() {
        return `this property won't be present in the instance, only in the prototype`
      }
    
      // this property is present in the instance
      instanceProp!: string
    
      // methods always exist in the prototype
      customMethod() {}
    }
  11. Render hexes using PixiJS

    master

    To render a honeycomb grid using PixiJS, iterate over the Grid and use the hex.corners array to draw shapes. Since hex.corners is an array of {x, y} objects, it is directly compatible with PIXI.Polygon.

    As with SVG, you can adjust the origin in defineHex (e.g., origin: 'topLeft') to change how coordinates are calculated relative to the hex's bounding box.

    import * as PIXI from 'pixi.js';
    import { defineHex, Grid, rectangle } from 'honeycomb-grid'
    
    const Hex = defineHex({ dimensions: 30, origin: 'topLeft' })
    const grid = new Grid(Hex, rectangle({ width: 10, height: 10 }))
    
    const app = new PIXI.Application({ backgroundAlpha: 0 })
    const graphics = new PIXI.Graphics()
    
    document.body.appendChild(app.view)
    graphics.lineStyle(1, 0x999999)
    
    grid.forEach(renderHex)
    app.stage.addChild(graphics)
    
    function renderHex(hex: Hex) {
        // PIXI.Polygon happens to be compatible with hex.corners
        graphics.drawShape(
            new PIXI.Polygon(hex.corners)
        )
    }
  12. Bail a traversal when a hex is missing

    master

    By default, if a traverser produces a hex that does not exist in the Grid, Honeycomb will skip that hex and continue searching for the next valid hex produced by the traverser.

    If you want the traversal to stop immediately upon encountering a hex that is not part of the grid, pass { bail: true } as the second argument to grid.traverse().

    // Stops traversing as soon as the traverser hits a hex not in the grid
    grid.traverse(spiralTraverser, { bail: true })