gridmesh guide

← gridmesh · API reference · source

gridmesh is a toolkit for rules that turn a grid of cells into geometry LOCALLY — a wall between two filled cells, a rounded edge, a surface — where each cell's geometry depends only on its near neighbours. It cuts the grid into square chunks, hands you one chunk at a time (the cells it owns, plus the neighbouring cells across its border that it may read), and keeps track of which chunks an edit made stale, so a change to one cell rebuilds one chunk rather than the world.

You supply the rule; gridmesh supplies the partition, the neighbour lookup, the dirty bookkeeping and a flat segment mesh to emit into. Coordinates are hex OFFSET coordinates: whole rows, every odd row shifted half a cell.

use gridmesh::*;

A rule: from every owned cell, one segment to each filled neighbour on its right-hand side (axes 0..2), so every edge is emitted once, by the cell at its left end.

fn build_chunk(f: const ChunkField, ci: const ChunkInput) -> SegMesh {
  m = seg_mesh_new();
  for ix in ci.cell_ixs {
    x = f.xs[ix] ?? 0;
    y = f.ys[ix] ?? 0;
    for d in 0..3 {
      nx = step_x(x, y, d, 1);
      ny = step_y(x, y, d, 1);
      if idx_at(f.cidx, nx, ny) >= 0 {
        emit_segment(m, 1, x as float, y as float, 0.0, nx as float, ny as float, 0.0, 7, ix);
      }
    }
  }
  m
}
fn main() {

Four cells in a row; chunks of 1 << 3 = 8 cells a side; a halo of one cell.

  xs: vector<integer> = [5, 6, 7, 8];
  ys: vector<integer> = [0, 0, 0, 0];
  f = field_new(0, xs, ys, 3, 1);
  world = seg_mesh_new();
  for ci in all_inputs(f, 1) {
    seg_mesh_append(world, build_chunk(f, ci));
  }
  println("full build: {seg_len(world)} segments from {len(all_inputs(f, 1))} chunks");
  assert(seg_len(world) == 3, "5-6, 6-7 and 7-8: the edge across the border once, by its owner");

Every section below is a function, and main calls each one: a section nothing calls still compiles, but its assertions never run.

  owned_and_halo();
  an_edit_rebuilds_one_chunk();
  a_border_edit_reaches_the_neighbour();
  walking_the_grid();
}

That prints:

  full build: 3 segments from 2 chunks

The cell at x = 8 is in the second chunk, and the edge 7-8 is emitted once — by the first chunk, which owns 7. That is the partition doing its job: each cell is owned by exactly one chunk, and a rule that emits only for its OWNED cells never doubles a border.

Owned cells and halo cells

A ChunkInput carries two lists that mean opposite things. cell_ixs are the cells this chunk owns: emit for these. halo_ixs are cells of OTHER chunks within halo_k of the border: a rule may read them as context, and must never emit for them.

fn owned_and_halo() {
  xs: vector<integer> = [5, 6, 7, 8];
  ys: vector<integer> = [0, 0, 0, 0];
  f = field_new(0, xs, ys, 3, 1);
  inputs = all_inputs(f, 1);
  assert(len(inputs) == 2, "two chunks hold cells");
  for ci in inputs {
    if ci.key.cx == 0 {
      assert(ci.cell_ixs == [0, 1, 2] && ci.halo_ixs == [3], "x 5..7 owned; it can see 8");
    } else {
      assert(ci.cell_ixs == [3] && ci.halo_ixs == [2], "x 8 owned; it can see 7");
    }
  }
  println("partition : every cell owned once, borders seen from both sides");
}

An edit rebuilds one chunk

An edit does not rebuild anything. field_add_cell, field_mark_dirty (a cell's payload changed) and field_remove_cell record the change and mark the chunks whose meshes it made stale. collect_dirty_inputs hands back those chunks — skipping any that hold no cells — and clear_dirty must come AFTER the rebuild, or every later collect returns them again.

fn an_edit_rebuilds_one_chunk() {
  xs: vector<integer> = [5, 6, 7, 8];
  ys: vector<integer> = [0, 0, 0, 0];
  f = field_new(0, xs, ys, 3, 1);
  field_add_cell(f, 9, 0);

Row 0 is within the halo of the row above, so chunk (1, -1) is marked too; it holds no cells, so it is not in the work-list.

  assert(dirty_count(f) == 2, "its own chunk and the empty one above");
  work = collect_dirty_inputs(f, 1);
  assert(len(work) == 1 && work[0].cell_ixs == [3, 4], "one chunk to rebuild: x 8 and the new 9");
  rebuilt = build_chunk(f, work[0]);
  assert(seg_len(rebuilt) == 1, "8-9 is the only edge it owns");
  clear_dirty(f);
  assert(len(collect_dirty_inputs(f, 1)) == 0, "cleared: nothing stale");
  println("edit      : one chunk rebuilt, {seg_len(rebuilt)} segment");
}

A border edit reaches the neighbour

A cell within halo_k of a chunk border is read by the chunk across it, so an edit there marks both. Recolouring x = 7, the last column of chunk 0, dirties chunk 1 as well.

fn a_border_edit_reaches_the_neighbour() {
  xs: vector<integer> = [5, 6, 7, 8];
  ys: vector<integer> = [0, 0, 0, 0];
  f = field_new(0, xs, ys, 3, 1);
  field_mark_dirty(f, 7, 0);
  assert(chunk_is_dirty(f, 7, 0) && chunk_is_dirty(f, 8, 0), "both sides of the border");
  assert(len(collect_dirty_inputs(f, 1)) == 2, "two chunks with cells to rebuild");
  println("border    : an edit at the edge rebuilds both sides");
}

Walking the grid

step_x and step_y move k steps along hex axis d (0..5) and are always used as a PAIR on the same (x, y): on odd rows the x of a diagonal step depends on the row. idx_at finds the cell at a coordinate, answering -1 — not null — for an empty one; nbr_count_idx counts a cell's filled neighbours.

fn walking_the_grid() {
  xs: vector<integer> = [0, 1, 0, 1];
  ys: vector<integer> = [0, 0, 1, 1];
  cidx = build_index(xs, ys);

Axis 1 from (0, 0) lands on row 1 at x 0; from (0, 1), an odd row, it lands on (1, 2).

  assert(step_x(0, 0, 1, 1) == 0 && step_y(0, 0, 1, 1) == 1, "even row: straight up");
  assert(step_x(0, 1, 1, 1) == 1 && step_y(0, 1, 1, 1) == 2, "odd row: shifted half a cell");
  assert(idx_at(cidx, 1, 1) == 3 && idx_at(cidx, 5, 5) == -1, "a miss is -1");
  assert(nbr_count_idx(cidx, 0, 0) == 2, "(0, 0) touches (1, 0) and (0, 1)");
  println("walk      : axis steps, -1 for a miss");
}

Where to go next