http://mathling.com/shape/grid  library module

http://mathling.com/shape/grid


Simple rectangular and hexagonal grids.

Copyright© Mary Holstege 2020-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)

February 2023
Status: Active

Imports

http://mathling.com/geometric/path
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy"
http://mathling.com/geometric/edge
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/core/config
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"

Functions

Function: slots-per-line
declare function slots-per-line($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*)) as xs:integer

Params
  • slot as map(xs:string,item()*)
  • canvas as map(xs:string,item()*)
Returns
  • xs:integer
declare function this:slots-per-line($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*)) as xs:integer
{
  let $slot-width := box:width($slot)
  let $slot-edge := box:edge($slot)
  let $canvas-width := box:width($canvas)
  let $canvas-edge := box:edge($canvas)
  let $end-x := $canvas-edge idiv $slot-width
  let $max-slots := $canvas-width idiv ($slot-width + $slot-edge)
  return $max-slots - $end-x
}

Function: lines-per-canvas
declare function lines-per-canvas($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*)) as xs:integer

Params
  • slot as map(xs:string,item()*)
  • canvas as map(xs:string,item()*)
Returns
  • xs:integer
declare function this:lines-per-canvas($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*)) as xs:integer
{
  let $slot-height := box:height($slot)
  let $slot-edge := box:edge($slot)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $end-y := $canvas-edge idiv $slot-height
  let $max-lines := $canvas-height idiv ($slot-height + $slot-edge)
  return $max-lines - $end-y
}

Function: array
declare function array($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*))


array()
Create a rectangular array

Params
  • slot as map(xs:string,item()*): slot canvas with height/width parameters
  • canvas as map(xs:string,item()*): array canvas with height/width/edge parameters
declare function this:array($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*))
{
  let $slot-width := box:width($slot)
  let $slot-height := box:height($slot)
  let $slot-edge := box:edge($slot)
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $max-lines := ($canvas-height - 2*$canvas-edge) idiv ($slot-height + $slot-edge)
  let $max-slots := ($canvas-width - 2*$canvas-edge) idiv ($slot-width + $slot-edge)
  return this:array($max-slots, $max-lines, $slot-edge, $canvas)
}

Function: array
declare function array($max-slots as xs:integer, $max-lines as xs:integer, $slot-edge as xs:double, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


array()
Construct an NxM array of points within the canvas, separated by the edge

Params
  • max-slots as xs:integer: number of columns
  • max-lines as xs:integer: number of rows
  • slot-edge as xs:double: extra spacing between points
  • canvas as map(xs:string,item()*): space to fill
Returns
  • map(xs:string,item()*)*
declare function this:array(
  $max-slots as xs:integer,
  $max-lines as xs:integer,
  $slot-edge as xs:double,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $slot-width := (($canvas-width - 2*$canvas-edge - ($max-slots - 1)*$slot-edge) idiv $max-slots)
  let $slot-height := (($canvas-height - 2*$canvas-edge - ($max-lines - 1)*$slot-edge) idiv $max-lines)
  (: Divide up the canvas into lines and slots, skipping over the edges :)
  let $start-x := $canvas-edge idiv $slot-width
  let $start-y := $canvas-edge idiv $slot-height
  let $end-x := $canvas-edge idiv $slot-width
  let $end-y := $canvas-edge idiv $slot-height
  let $x-offset := $min-x + $slot-edge*$start-x
  let $y-offset := $min-y + $slot-edge*$start-y
  for $line in $start-y to $max-lines - (1 + $end-y)
  let $trans-y := $y-offset + $line * ($slot-height + $slot-edge)
  for $slot in $start-x to $max-slots - (1 + $end-x)
  let $trans-x := $x-offset + $slot * ($slot-width + $slot-edge)
  return (
    point:point($trans-x,$trans-y)
  )
}

Function: array-rectangles
declare function array-rectangles($max-slots as xs:integer, $max-lines as xs:integer, $slot-edge as xs:integer, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


array-rectangles()
Construct an NxM array of rectangles within the canvas, separated by the edge
Like array() except we return the rectangles, not the grid points.
We return the inner rectangles (with the gaps between them) as polygons.

Params
  • max-slots as xs:integer: number of columns
  • max-lines as xs:integer: number of rows
  • slot-edge as xs:integer: extra spacing between rectangles
  • canvas as map(xs:string,item()*): space to fill
Returns
  • map(xs:string,item()*)*
declare function this:array-rectangles(
  $max-slots as xs:integer,
  $max-lines as xs:integer,
  $slot-edge as xs:integer,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $slot-width := ($canvas-width - 2*$canvas-edge - ($max-slots - 1)*$slot-edge) idiv $max-slots
  let $slot-height := ($canvas-height - 2*$canvas-edge - ($max-lines - 1)*$slot-edge) idiv $max-lines
  (: Divide up the canvas into lines and slots, skipping over the edges :)
  let $start-x := $canvas-edge idiv $slot-width
  let $start-y := $canvas-edge idiv $slot-height
  let $end-x := $canvas-edge idiv $slot-width
  let $end-y := $canvas-edge idiv $slot-height
  let $x-offset := $min-x + $slot-edge*$start-x
  let $y-offset := $min-y + $slot-edge*$start-y
  for $line in $start-y to $max-lines - (1 + $end-y)
  let $y1 := $y-offset + $line * ($slot-height + $slot-edge)
  let $y2 := $y-offset + ($line + 1) * ($slot-height + $slot-edge) - $slot-edge
  for $slot in $start-x to $max-slots - (1 + $end-x)
  let $x1 := $x-offset + $slot * ($slot-width + $slot-edge)
  let $x2 := $x-offset + ($slot + 1) * ($slot-width + $slot-edge) - $slot-edge
  return (
    path:polygon(
      box:edges(box:box($x1, $y1, $x2, $y2))
    )
  )
}

Function: array-boxes
declare function array-boxes($max-slots as xs:integer, $max-lines as xs:integer, $slot-edge as xs:integer, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


array-boxes()
Construct an NxM array of boxes within the canvas, separated by the edge
Like array() except we return the rectangles, not the grid points.
Like array-rectangles() except we return the rectangles as boxes not polygons.

Params
  • max-slots as xs:integer: number of columns
  • max-lines as xs:integer: number of rows
  • slot-edge as xs:integer: extra spacing between rectangles
  • canvas as map(xs:string,item()*): space to fill
Returns
  • map(xs:string,item()*)*
declare function this:array-boxes(
  $max-slots as xs:integer,
  $max-lines as xs:integer,
  $slot-edge as xs:integer,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $slot-width := ($canvas-width - 2*$canvas-edge - ($max-slots - 1)*$slot-edge) idiv $max-slots
  let $slot-height := ($canvas-height - 2*$canvas-edge - ($max-lines - 1)*$slot-edge) idiv $max-lines
  (: Divide up the canvas into lines and slots, skipping over the edges :)
  let $start-x := $canvas-edge idiv $slot-width
  let $start-y := $canvas-edge idiv $slot-height
  let $end-x := $canvas-edge idiv $slot-width
  let $end-y := $canvas-edge idiv $slot-height
  let $x-offset := $min-x + $slot-edge*$start-x
  let $y-offset := $min-y + $slot-edge*$start-y
  for $line in $start-y to $max-lines - (1 + $end-y)
  let $y1 := $y-offset + $line * ($slot-height + $slot-edge)
  let $y2 := $y-offset + ($line + 1) * ($slot-height + $slot-edge) - $slot-edge
  for $slot in $start-x to $max-slots - (1 + $end-x)
  let $x1 := $x-offset + $slot * ($slot-width + $slot-edge)
  let $x2 := $x-offset + ($slot + 1) * ($slot-width + $slot-edge) - $slot-edge
  return (
     box:box($x1, $y1, $x2, $y2)
  )
}

Function: grid
declare function grid($grid-size as xs:double, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


grid()
Create a set of grid lines with the given scale. Useful for debugging.

Params
  • grid-size as xs:double: Spacing between grid lines
  • canvas as map(xs:string,item()*): grid canvas with height/width parameters
Returns
  • map(xs:string,item()*)*
declare function this:grid($grid-size as xs:double, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $max-lines := $canvas-height idiv $grid-size
  let $max-slots := $canvas-width idiv $grid-size
  return (
    for $slot in 0 to $max-slots - 1 return (
      edge:edge(
        point:point($slot*$grid-size, 0),
        point:point($slot*$grid-size, $canvas-height)
      )
    )
    ,
    for $line in 0 to $max-lines - 1 return (
      edge:edge(
        point:point(0, $line * $grid-size),
        point:point($canvas-width, $line * $grid-size)
      )
    )
  )
}

Function: hex-grid
declare function hex-grid($radius as xs:double, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


hex-grid()
Return the centers of hexagons in a hexagonal gridding of the space.
Center-to-center horizontally in a row = 3r
Vertical offset from row to row = ½r√3
Horizonal offset from row to row = 3r/2

Params
  • radius as xs:double
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:hex-grid(
  $radius as xs:double,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $max-rows as xs:integer :=
    (box:height($canvas) - 2*box:edge($canvas) - math:sqrt(3)*$radius div 2) idiv (math:sqrt(3)*$radius div 2)
  let $max-columns as xs:integer :=
    1 + (box:width($canvas) - 2*box:edge($canvas)) idiv (3*$radius)
  for $row in 1 to $max-rows
  let $y := box:edge($canvas) + box:min-y($canvas) + $row * math:sqrt(3) * $radius div 2
  let $current-max :=
    if ($row mod 2 = 0) then $max-columns - 1 else $max-columns
  let $start-offset :=
    box:edge($canvas) + box:min-x($canvas) + $radius + (if ($row mod 2 = 0) then 3*$radius div 2 else 0)
  for $column in 1 to $current-max
  let $x := $start-offset + ($column - 1) * 3 * $radius
  return point:point($x, $y)
}

Function: navigable-hex-grid
declare function navigable-hex-grid($radius as xs:double, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • radius as xs:double
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:navigable-hex-grid(
  $radius as xs:double,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $rows as xs:integer :=
    (box:height($canvas) - 2*box:edge($canvas) - math:sqrt(3)*$radius div 2) idiv (math:sqrt(3)*$radius div 2)
  let $columns as xs:integer :=
    1 + (box:width($canvas) - 2*box:edge($canvas)) idiv (3*$radius)
  let $points := this:hex-grid($radius, $canvas)=>geom:snap()
  return (
    map {
      "kind": "hex-grid",
      "rows": $rows,
      "columns": $columns,
      "radius": $radius,
      "points": $points,
      "n": count($points)
    }
  )
}

Function: hex-point
declare function hex-point($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as map(xs:string,item()*)?

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • map(xs:string,item()*)?
declare function this:hex-point(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as map(xs:string,item()*)?
{
  let $columns := $hexgrid("columns")
  let $ix :=
    if ($row mod 2 = 0) then (
      ($row idiv 2) * $columns + ($row idiv 2 - 1) * ($columns - 1) + $column
    ) else (
      ($row idiv 2) * $columns + ($row idiv 2) * ($columns - 1) + $column
    )
  return $hexgrid("points")[$ix]
}

Function: hex-point
declare function hex-point($hexgrid as map(xs:string,item()*), $coords as xs:integer*) as map(xs:string,item()*)?

Params
  • hexgrid as map(xs:string,item()*)
  • coords as xs:integer*
Returns
  • map(xs:string,item()*)?
declare function this:hex-point(
  $hexgrid as map(xs:string,item()*),
  $coords as xs:integer*
) as map(xs:string,item()*)?
{
  this:hex-point($hexgrid, $coords[1], $coords[2])
}

Function: hexagon
declare function hexagon($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as map(xs:string,item()*)?

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • map(xs:string,item()*)?
declare function this:hexagon(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as map(xs:string,item()*)?
{
  let $radius := $hexgrid("radius")
  let $point := $hexgrid=>this:hex-point($row, $column)
  where exists($point)
  return (
    path:polygon(
      (
        for $angle in (0, 60, 120, 180, 240, 300) return (
          $point=>point:destination($angle, $radius)=>point:snap()          
        )
      )=>edge:to-edges()
    )
  )
}

Function: hex-pix
declare function hex-pix($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer
declare function this:hex-pix(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer
{
  let $columns := $hexgrid("columns")
  let $ix :=
    if ($row mod 2 = 0) then (
      ($row idiv 2) * $columns + ($row idiv 2 - 1) * ($columns - 1) + $column
    ) else (
      ($row idiv 2) * $columns + ($row idiv 2) * ($columns - 1) + $column
    )
  return $ix
}

Function: hex-pix
declare function hex-pix($hexgrid as map(xs:string,item()*), $coords as xs:integer*) as xs:integer

Params
  • hexgrid as map(xs:string,item()*)
  • coords as xs:integer*
Returns
  • xs:integer
declare function this:hex-pix(
  $hexgrid as map(xs:string,item()*),
  $coords as xs:integer*
) as xs:integer
{
  this:hex-pix($hexgrid, $coords[1], $coords[2])
}

Function: down-left
declare function down-left($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer*

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer*
declare function this:down-left(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = $hexgrid("rows")) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row + 1, $column)) then ()
    else ($row + 1, $column)
  )
  else (
    if ($column = 1) then ()
    else if (this:excluded($hexgrid, $row + 1, $column - 1)) then ()
    else ($row + 1, $column - 1)
  )
}

Function: up-left
declare function up-left($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer*

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer*
declare function this:up-left(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = 1) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row - 1, $column)) then ()
    else ($row - 1, $column)
  )
  else (
    if ($column = 1) then ()
    else if (this:excluded($hexgrid, $row - 1, $column - 1)) then ()
    else ($row - 1, $column - 1)
  )
}

Function: down-right
declare function down-right($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer*

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer*
declare function this:down-right(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = $hexgrid("rows")) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row + 1, $column + 1)) then ()
    else ($row + 1, $column + 1)
  ) else (
    if ($column = $hexgrid("columns")) then ()
    else if (this:excluded($hexgrid, $row + 1, $column)) then ()
    else ($row + 1, $column)
  )
}

Function: up-right
declare function up-right($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer*

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer*
declare function this:up-right(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = 1) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row - 1, $column + 1)) then ()
    else ($row - 1, $column + 1)
  ) else (
    if ($column = $hexgrid("columns")) then ()
    else if (this:excluded($hexgrid, $row - 1, $column)) then ()
    else ($row - 1, $column)
  )
}

Function: down-straight
declare function down-straight($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer*

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer*
declare function this:down-straight(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row >= $hexgrid("rows") - 1) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row + 2, $column)) then ()
    else ($row + 2, $column)
  )
  else (
    if (this:excluded($hexgrid, $row + 2, $column)) then ()
    else ($row + 2, $column)
  )
}

Function: up-straight
declare function up-straight($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:integer*

Params
  • hexgrid as map(xs:string,item()*)
  • row as xs:integer
  • column as xs:integer
Returns
  • xs:integer*
declare function this:up-straight(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row <= 2) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row - 2, $column)) then ()
    else ($row - 2, $column)
  )
  else (
    if (this:excluded($hexgrid, $row - 2, $column)) then ()
    else ($row - 2, $column)
  )
}

Function: hex-triads
declare function hex-triads($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as array(array(xs:integer))*


hex-triads()
Get the triangles around the given point in the hex grid, returned
as an array of arrays of (row, column) grid references.

Params
  • hexgrid as map(xs:string,item()*): the navigable hex grid
  • row as xs:integer: target row in grid
  • column as xs:integer: target column in grid
Returns
  • array(array(xs:integer))*
declare function this:hex-triads(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as array(array(xs:integer))*
{
  (:
   : Get the hex points around the focus:
   :
   :             1
   :          6     2
   :             *
   :          5     3
   :             4
   :)
  let $v1 := $hexgrid=>this:up-straight($row, $column)
  let $v2 := $hexgrid=>this:up-right($row, $column)
  let $v3 := $hexgrid=>this:down-right($row, $column)
  let $v4 := $hexgrid=>this:down-straight($row, $column)
  let $v5 := $hexgrid=>this:down-left($row, $column)
  let $v6 := $hexgrid=>this:up-left($row, $column)
  (: 
   : Now make triangles from the ones that exist
   : These "triangles" are just the grid references in an array of pairs
   : Order them so that the first vertex is the lowest one
   :)
  let $focus := array {($row, $column)}
  return (
    if (exists($v1)) then (
      if (exists($v2)) then array{ $focus, array {$v2}, array {$v1} } else (),
      if (exists($v6)) then array{ $focus, array {$v1}, array {$v6} } else ()
    ) else (),
    if (exists($v3)) then (
      if (exists($v2)) then array{ array {$v3}, array {$v2}, $focus } else (),
      if (exists($v4)) then array{ array {$v4}, array {$v3}, $focus } else ()
    ) else (),
    if (exists($v5)) then (
      if (exists($v4)) then array{ array {$v4}, $focus, array {$v5} } else (),
      if (exists($v6)) then array{ array {$v5}, $focus, array {$v6} } else ()
    ) else ()
  )
}

Function: hex-subgrid
declare function hex-subgrid($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as map(xs:string,item()*)


hex-subgrid()
Construct a subgrid for the given hexagon in the grid. We will end up
with a couple of extra points in the first row to make the navigation
work properly: be sure to use excluded() or hex-points() to ignore them.

Params
  • hexgrid as map(xs:string,item()*): the navigable hex grid
  • row as xs:integer: target row in grid
  • column as xs:integer: target column in grid
Returns
  • map(xs:string,item()*)
declare function this:hex-subgrid(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as map(xs:string,item()*)
{
  let $center := this:hex-point($hexgrid, $row, $column)
  return this:hex-subgrid($hexgrid, $center)
}

Function: hex-subgrid
declare function hex-subgrid($hexgrid as map(xs:string,item()*), $center as map(xs:string,item()*)) as map(xs:string,item()*)


hex-subgrid()
Construct a subgrid for the given hexagon in the grid. We will end up
with a couple of extra points in the first row to make the navigation
work properly: be sure to use excluded() or hex-points() to ignore them.

Params
  • hexgrid as map(xs:string,item()*): the navigable hex grid
  • center as map(xs:string,item()*): target point in grid
Returns
  • map(xs:string,item()*)
declare function this:hex-subgrid(
  $hexgrid as map(xs:string,item()*),
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $radius := $hexgrid("radius") div 3
  let $v := $radius * math:sqrt(3) div 2
  let $h := 3 * $radius div 2
  let $points := (
    $center=>geom:translate(-$h, -3 * $v), (: outside grid :)
    $center=>geom:translate($h, -3 * $v),  (: row 1: outside grid :)
    $center=>geom:translate(0, -2 * $v),   (: row 2: first inside :)
    $center=>geom:translate(-$h, -$v),     (: row 3: inside :)
    $center=>geom:translate($h, -$v),      (: row 3: inside :)
    $center,                               (: row 4: inside :)
    $center=>geom:translate(-$h, $v),      (: row 5: inside :)
    $center=>geom:translate($h, $v),       (: row 5: inside :)
    $center=>geom:translate(0, 2 * $v)     (: row 6: inside :)
  )
  return (
    map {
      "kind": "hex-grid",
      "rows": 6,
      "columns": 2,
      "radius": $radius,
      "points": $points=>geom:snap(),
      "n": count($points) - 2,
      "exclusions": (1, 2)
    }
  )
}

Function: excluded
declare function excluded($hexgrid as map(xs:string,item()*), $pix as xs:integer) as xs:boolean


excluded()
Is the point at this index excluded?

Params
  • hexgrid as map(xs:string,item()*): the navigable hex grid
  • pix as xs:integer: target point index
Returns
  • xs:boolean
declare function this:excluded(
  $hexgrid as map(xs:string,item()*),
  $pix as xs:integer
) as xs:boolean
{
  $pix = $hexgrid("exclusions") or $pix > $hexgrid("n")
}

Function: excluded
declare function excluded($hexgrid as map(xs:string,item()*), $row as xs:integer, $column as xs:integer) as xs:boolean


excluded()
Is the point at this index excluded?

Params
  • hexgrid as map(xs:string,item()*): the navigable hex grid
  • row as xs:integer: target row in grid
  • column as xs:integer: target column in grid
Returns
  • xs:boolean
declare function this:excluded(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:boolean
{
  let $pix := this:hex-pix($hexgrid, $row, $column)
  return $pix = $hexgrid("exclusions") or $pix > $hexgrid("n")
}

Function: hex-points
declare function hex-points($hexgrid as map(xs:string,item()*)) as map(xs:string,item()*)*


hex-points()
The non-excluded points in the hex grid.

Params
  • hexgrid as map(xs:string,item()*): the navigable hex grid
Returns
  • map(xs:string,item()*)*
declare function this:hex-points(
  $hexgrid as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  $hexgrid("points")[not(position() = $hexgrid("exclusions"))]
}

Function: hexagons
declare function hexagons($hexgrid as map(xs:string,item()*)) as map(xs:string,item()*)*


hexagons()
Return the hexagon tiling associated with hexgrid.

Params
  • hexgrid as map(xs:string,item()*) the hexagonal grid
Returns
  • map(xs:string,item()*)*
declare function this:hexagons(
  $hexgrid as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $points := this:hex-points($hexgrid)
  let $radius := $hexgrid("radius")
  for $point in $points return (
    path:polygon(
      (
        for $angle in (0, 60, 120, 180, 240, 300) return (
          $point=>point:destination($angle, $radius)=>point:snap()          
        )
      )=>edge:to-edges()
    )
  )
}

Original Source Code

xquery version "3.1";
(:~
 : Simple rectangular and hexagonal grids.
 :
 : Copyright© Mary Holstege 2020-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since February 2023
 : @custom:Status Active
 :)
module namespace this="http://mathling.com/shape/grid"; 

import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";

declare namespace svg="http://www.w3.org/2000/svg";

declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace array="http://www.w3.org/2005/xpath-functions/array";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";

declare function this:slots-per-line($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*)) as xs:integer
{
  let $slot-width := box:width($slot)
  let $slot-edge := box:edge($slot)
  let $canvas-width := box:width($canvas)
  let $canvas-edge := box:edge($canvas)
  let $end-x := $canvas-edge idiv $slot-width
  let $max-slots := $canvas-width idiv ($slot-width + $slot-edge)
  return $max-slots - $end-x
};

declare function this:lines-per-canvas($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*)) as xs:integer
{
  let $slot-height := box:height($slot)
  let $slot-edge := box:edge($slot)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $end-y := $canvas-edge idiv $slot-height
  let $max-lines := $canvas-height idiv ($slot-height + $slot-edge)
  return $max-lines - $end-y
};

(:~
 : array()
 : Create a rectangular array
 : 
 : @param $slot: slot canvas with height/width parameters
 : @param $canvas: array canvas with height/width/edge parameters
 :)
declare function this:array($slot as map(xs:string,item()*), $canvas as map(xs:string,item()*))
{
  let $slot-width := box:width($slot)
  let $slot-height := box:height($slot)
  let $slot-edge := box:edge($slot)
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $max-lines := ($canvas-height - 2*$canvas-edge) idiv ($slot-height + $slot-edge)
  let $max-slots := ($canvas-width - 2*$canvas-edge) idiv ($slot-width + $slot-edge)
  return this:array($max-slots, $max-lines, $slot-edge, $canvas)
};

(:~
 : array()
 : Construct an NxM array of points within the canvas, separated by the edge
 : 
 : @param $max-slots: number of columns
 : @param $max-lines: number of rows
 : @param $slot-edge: extra spacing between points
 : @param $canvas: space to fill
 :)
declare function this:array(
  $max-slots as xs:integer,
  $max-lines as xs:integer,
  $slot-edge as xs:double,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $slot-width := (($canvas-width - 2*$canvas-edge - ($max-slots - 1)*$slot-edge) idiv $max-slots)
  let $slot-height := (($canvas-height - 2*$canvas-edge - ($max-lines - 1)*$slot-edge) idiv $max-lines)
  (: Divide up the canvas into lines and slots, skipping over the edges :)
  let $start-x := $canvas-edge idiv $slot-width
  let $start-y := $canvas-edge idiv $slot-height
  let $end-x := $canvas-edge idiv $slot-width
  let $end-y := $canvas-edge idiv $slot-height
  let $x-offset := $min-x + $slot-edge*$start-x
  let $y-offset := $min-y + $slot-edge*$start-y
  for $line in $start-y to $max-lines - (1 + $end-y)
  let $trans-y := $y-offset + $line * ($slot-height + $slot-edge)
  for $slot in $start-x to $max-slots - (1 + $end-x)
  let $trans-x := $x-offset + $slot * ($slot-width + $slot-edge)
  return (
    point:point($trans-x,$trans-y)
  )
};

(:~
 : array-rectangles()
 : Construct an NxM array of rectangles within the canvas, separated by the edge
 : Like array() except we return the rectangles, not the grid points.
 : We return the inner rectangles (with the gaps between them) as polygons.
 : 
 : @param $max-slots: number of columns
 : @param $max-lines: number of rows
 : @param $slot-edge: extra spacing between rectangles
 : @param $canvas: space to fill
 :)
declare function this:array-rectangles(
  $max-slots as xs:integer,
  $max-lines as xs:integer,
  $slot-edge as xs:integer,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $slot-width := ($canvas-width - 2*$canvas-edge - ($max-slots - 1)*$slot-edge) idiv $max-slots
  let $slot-height := ($canvas-height - 2*$canvas-edge - ($max-lines - 1)*$slot-edge) idiv $max-lines
  (: Divide up the canvas into lines and slots, skipping over the edges :)
  let $start-x := $canvas-edge idiv $slot-width
  let $start-y := $canvas-edge idiv $slot-height
  let $end-x := $canvas-edge idiv $slot-width
  let $end-y := $canvas-edge idiv $slot-height
  let $x-offset := $min-x + $slot-edge*$start-x
  let $y-offset := $min-y + $slot-edge*$start-y
  for $line in $start-y to $max-lines - (1 + $end-y)
  let $y1 := $y-offset + $line * ($slot-height + $slot-edge)
  let $y2 := $y-offset + ($line + 1) * ($slot-height + $slot-edge) - $slot-edge
  for $slot in $start-x to $max-slots - (1 + $end-x)
  let $x1 := $x-offset + $slot * ($slot-width + $slot-edge)
  let $x2 := $x-offset + ($slot + 1) * ($slot-width + $slot-edge) - $slot-edge
  return (
    path:polygon(
      box:edges(box:box($x1, $y1, $x2, $y2))
    )
  )
};

(:~
 : array-boxes()
 : Construct an NxM array of boxes within the canvas, separated by the edge
 : Like array() except we return the rectangles, not the grid points.
 : Like array-rectangles() except we return the rectangles as boxes not polygons.
 : 
 : @param $max-slots: number of columns
 : @param $max-lines: number of rows
 : @param $slot-edge: extra spacing between rectangles
 : @param $canvas: space to fill
 :)
declare function this:array-boxes(
  $max-slots as xs:integer,
  $max-lines as xs:integer,
  $slot-edge as xs:integer,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $canvas-edge := box:edge($canvas)
  let $min-x := box:min-x($canvas) + $canvas-edge
  let $min-y := box:min-y($canvas) + $canvas-edge
  let $slot-width := ($canvas-width - 2*$canvas-edge - ($max-slots - 1)*$slot-edge) idiv $max-slots
  let $slot-height := ($canvas-height - 2*$canvas-edge - ($max-lines - 1)*$slot-edge) idiv $max-lines
  (: Divide up the canvas into lines and slots, skipping over the edges :)
  let $start-x := $canvas-edge idiv $slot-width
  let $start-y := $canvas-edge idiv $slot-height
  let $end-x := $canvas-edge idiv $slot-width
  let $end-y := $canvas-edge idiv $slot-height
  let $x-offset := $min-x + $slot-edge*$start-x
  let $y-offset := $min-y + $slot-edge*$start-y
  for $line in $start-y to $max-lines - (1 + $end-y)
  let $y1 := $y-offset + $line * ($slot-height + $slot-edge)
  let $y2 := $y-offset + ($line + 1) * ($slot-height + $slot-edge) - $slot-edge
  for $slot in $start-x to $max-slots - (1 + $end-x)
  let $x1 := $x-offset + $slot * ($slot-width + $slot-edge)
  let $x2 := $x-offset + ($slot + 1) * ($slot-width + $slot-edge) - $slot-edge
  return (
     box:box($x1, $y1, $x2, $y2)
  )
};

(:~
 : grid()
 : Create a set of grid lines with the given scale. Useful for debugging.
 :
 : @param $grid-size: Spacing between grid lines
 : @param $canvas: grid canvas with height/width parameters
 :)
declare function this:grid($grid-size as xs:double, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $canvas-width := box:width($canvas)
  let $canvas-height := box:height($canvas)
  let $max-lines := $canvas-height idiv $grid-size
  let $max-slots := $canvas-width idiv $grid-size
  return (
    for $slot in 0 to $max-slots - 1 return (
      edge:edge(
        point:point($slot*$grid-size, 0),
        point:point($slot*$grid-size, $canvas-height)
      )
    )
    ,
    for $line in 0 to $max-lines - 1 return (
      edge:edge(
        point:point(0, $line * $grid-size),
        point:point($canvas-width, $line * $grid-size)
      )
    )
  )
};

(:~
 : hex-grid()
 : Return the centers of hexagons in a hexagonal gridding of the space.
 : Center-to-center horizontally in a row = 3r
 : Vertical offset from row to row = ½r√3
 : Horizonal offset from row to row = 3r/2
 :)
declare function this:hex-grid(
  $radius as xs:double,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $max-rows as xs:integer :=
    (box:height($canvas) - 2*box:edge($canvas) - math:sqrt(3)*$radius div 2) idiv (math:sqrt(3)*$radius div 2)
  let $max-columns as xs:integer :=
    1 + (box:width($canvas) - 2*box:edge($canvas)) idiv (3*$radius)
  for $row in 1 to $max-rows
  let $y := box:edge($canvas) + box:min-y($canvas) + $row * math:sqrt(3) * $radius div 2
  let $current-max :=
    if ($row mod 2 = 0) then $max-columns - 1 else $max-columns
  let $start-offset :=
    box:edge($canvas) + box:min-x($canvas) + $radius + (if ($row mod 2 = 0) then 3*$radius div 2 else 0)
  for $column in 1 to $current-max
  let $x := $start-offset + ($column - 1) * 3 * $radius
  return point:point($x, $y)
};

declare function this:navigable-hex-grid(
  $radius as xs:double,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $rows as xs:integer :=
    (box:height($canvas) - 2*box:edge($canvas) - math:sqrt(3)*$radius div 2) idiv (math:sqrt(3)*$radius div 2)
  let $columns as xs:integer :=
    1 + (box:width($canvas) - 2*box:edge($canvas)) idiv (3*$radius)
  let $points := this:hex-grid($radius, $canvas)=>geom:snap()
  return (
    map {
      "kind": "hex-grid",
      "rows": $rows,
      "columns": $columns,
      "radius": $radius,
      "points": $points,
      "n": count($points)
    }
  )
};

declare function this:hex-point(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as map(xs:string,item()*)?
{
  let $columns := $hexgrid("columns")
  let $ix :=
    if ($row mod 2 = 0) then (
      ($row idiv 2) * $columns + ($row idiv 2 - 1) * ($columns - 1) + $column
    ) else (
      ($row idiv 2) * $columns + ($row idiv 2) * ($columns - 1) + $column
    )
  return $hexgrid("points")[$ix]
};

declare function this:hex-point(
  $hexgrid as map(xs:string,item()*),
  $coords as xs:integer*
) as map(xs:string,item()*)?
{
  this:hex-point($hexgrid, $coords[1], $coords[2])
};

declare function this:hexagon(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as map(xs:string,item()*)?
{
  let $radius := $hexgrid("radius")
  let $point := $hexgrid=>this:hex-point($row, $column)
  where exists($point)
  return (
    path:polygon(
      (
        for $angle in (0, 60, 120, 180, 240, 300) return (
          $point=>point:destination($angle, $radius)=>point:snap()          
        )
      )=>edge:to-edges()
    )
  )
};

declare function this:hex-pix(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer
{
  let $columns := $hexgrid("columns")
  let $ix :=
    if ($row mod 2 = 0) then (
      ($row idiv 2) * $columns + ($row idiv 2 - 1) * ($columns - 1) + $column
    ) else (
      ($row idiv 2) * $columns + ($row idiv 2) * ($columns - 1) + $column
    )
  return $ix
};

declare function this:hex-pix(
  $hexgrid as map(xs:string,item()*),
  $coords as xs:integer*
) as xs:integer
{
  this:hex-pix($hexgrid, $coords[1], $coords[2])
};

declare function this:down-left(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = $hexgrid("rows")) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row + 1, $column)) then ()
    else ($row + 1, $column)
  )
  else (
    if ($column = 1) then ()
    else if (this:excluded($hexgrid, $row + 1, $column - 1)) then ()
    else ($row + 1, $column - 1)
  )
};

declare function this:up-left(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = 1) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row - 1, $column)) then ()
    else ($row - 1, $column)
  )
  else (
    if ($column = 1) then ()
    else if (this:excluded($hexgrid, $row - 1, $column - 1)) then ()
    else ($row - 1, $column - 1)
  )
};

declare function this:down-right(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = $hexgrid("rows")) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row + 1, $column + 1)) then ()
    else ($row + 1, $column + 1)
  ) else (
    if ($column = $hexgrid("columns")) then ()
    else if (this:excluded($hexgrid, $row + 1, $column)) then ()
    else ($row + 1, $column)
  )
};

declare function this:up-right(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row = 1) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row - 1, $column + 1)) then ()
    else ($row - 1, $column + 1)
  ) else (
    if ($column = $hexgrid("columns")) then ()
    else if (this:excluded($hexgrid, $row - 1, $column)) then ()
    else ($row - 1, $column)
  )
};

declare function this:down-straight(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row >= $hexgrid("rows") - 1) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row + 2, $column)) then ()
    else ($row + 2, $column)
  )
  else (
    if (this:excluded($hexgrid, $row + 2, $column)) then ()
    else ($row + 2, $column)
  )
};

declare function this:up-straight(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:integer* (: (row, column) or empty :)
{
  if ($row <= 2) then ()
  else if ($row mod 2 = 0) then (
    if ($column = $hexgrid("columns")) then () (: Shouldn't happen :)
    else if (this:excluded($hexgrid, $row - 2, $column)) then ()
    else ($row - 2, $column)
  )
  else (
    if (this:excluded($hexgrid, $row - 2, $column)) then ()
    else ($row - 2, $column)
  )
};

(:~
 : hex-triads()
 : Get the triangles around the given point in the hex grid, returned
 : as an array of arrays of (row, column) grid references.
 :
 : @param $hexgrid: the navigable hex grid
 : @param $row: target row in grid
 : @param $column: target column in grid
 :)
declare function this:hex-triads(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as array(array(xs:integer))*
{
  (:
   : Get the hex points around the focus:
   :
   :             1
   :          6     2
   :             *
   :          5     3
   :             4
   :)
  let $v1 := $hexgrid=>this:up-straight($row, $column)
  let $v2 := $hexgrid=>this:up-right($row, $column)
  let $v3 := $hexgrid=>this:down-right($row, $column)
  let $v4 := $hexgrid=>this:down-straight($row, $column)
  let $v5 := $hexgrid=>this:down-left($row, $column)
  let $v6 := $hexgrid=>this:up-left($row, $column)
  (: 
   : Now make triangles from the ones that exist
   : These "triangles" are just the grid references in an array of pairs
   : Order them so that the first vertex is the lowest one
   :)
  let $focus := array {($row, $column)}
  return (
    if (exists($v1)) then (
      if (exists($v2)) then array{ $focus, array {$v2}, array {$v1} } else (),
      if (exists($v6)) then array{ $focus, array {$v1}, array {$v6} } else ()
    ) else (),
    if (exists($v3)) then (
      if (exists($v2)) then array{ array {$v3}, array {$v2}, $focus } else (),
      if (exists($v4)) then array{ array {$v4}, array {$v3}, $focus } else ()
    ) else (),
    if (exists($v5)) then (
      if (exists($v4)) then array{ array {$v4}, $focus, array {$v5} } else (),
      if (exists($v6)) then array{ array {$v5}, $focus, array {$v6} } else ()
    ) else ()
  )
};

(:~
 : hex-subgrid()
 : Construct a subgrid for the given hexagon in the grid. We will end up
 : with a couple of extra points in the first row to make the navigation
 : work properly: be sure to use excluded() or hex-points() to ignore them.
 :
 : @param $hexgrid: the navigable hex grid
 : @param $row: target row in grid
 : @param $column: target column in grid
 :)
declare function this:hex-subgrid(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as map(xs:string,item()*)
{
  let $center := this:hex-point($hexgrid, $row, $column)
  return this:hex-subgrid($hexgrid, $center)
};

(:~
 : hex-subgrid()
 : Construct a subgrid for the given hexagon in the grid. We will end up
 : with a couple of extra points in the first row to make the navigation
 : work properly: be sure to use excluded() or hex-points() to ignore them.
 :
 : @param $hexgrid: the navigable hex grid
 : @param $center: target point in grid
 :)
declare function this:hex-subgrid(
  $hexgrid as map(xs:string,item()*),
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $radius := $hexgrid("radius") div 3
  let $v := $radius * math:sqrt(3) div 2
  let $h := 3 * $radius div 2
  let $points := (
    $center=>geom:translate(-$h, -3 * $v), (: outside grid :)
    $center=>geom:translate($h, -3 * $v),  (: row 1: outside grid :)
    $center=>geom:translate(0, -2 * $v),   (: row 2: first inside :)
    $center=>geom:translate(-$h, -$v),     (: row 3: inside :)
    $center=>geom:translate($h, -$v),      (: row 3: inside :)
    $center,                               (: row 4: inside :)
    $center=>geom:translate(-$h, $v),      (: row 5: inside :)
    $center=>geom:translate($h, $v),       (: row 5: inside :)
    $center=>geom:translate(0, 2 * $v)     (: row 6: inside :)
  )
  return (
    map {
      "kind": "hex-grid",
      "rows": 6,
      "columns": 2,
      "radius": $radius,
      "points": $points=>geom:snap(),
      "n": count($points) - 2,
      "exclusions": (1, 2)
    }
  )
};

(:~
 : excluded()
 : Is the point at this index excluded?
 :
 : @param $hexgrid: the navigable hex grid
 : @param $pix: target point index
 :)
declare function this:excluded(
  $hexgrid as map(xs:string,item()*),
  $pix as xs:integer
) as xs:boolean
{
  $pix = $hexgrid("exclusions") or $pix > $hexgrid("n")
};

(:~
 : excluded()
 : Is the point at this index excluded?
 :
 : @param $hexgrid: the navigable hex grid
 : @param $row: target row in grid
 : @param $column: target column in grid
 :)
declare function this:excluded(
  $hexgrid as map(xs:string,item()*),
  $row as xs:integer,
  $column as xs:integer
) as xs:boolean
{
  let $pix := this:hex-pix($hexgrid, $row, $column)
  return $pix = $hexgrid("exclusions") or $pix > $hexgrid("n")
};

(:~
 : hex-points()
 : The non-excluded points in the hex grid.
 :
 : @param $hexgrid: the navigable hex grid
 :)
declare function this:hex-points(
  $hexgrid as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  $hexgrid("points")[not(position() = $hexgrid("exclusions"))]
};

(:~
 : hexagons()
 : Return the hexagon tiling associated with hexgrid.
 :
 : @param $hexgrid the hexagonal grid
 :)
declare function this:hexagons(
  $hexgrid as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $points := this:hex-points($hexgrid)
  let $radius := $hexgrid("radius")
  for $point in $points return (
    path:polygon(
      (
        for $angle in (0, 60, 120, 180, 240, 300) return (
          $point=>point:destination($angle, $radius)=>point:snap()          
        )
      )=>edge:to-edges()
    )
  )
};