magicciv/public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md
Natalie a2a28fdf34 feat(@projects/@magic-civilization): mark hex direction mapping as complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-01 00:49:46 -04:00

4.9 KiB
Raw Permalink Blame History

Hex Direction Conventions — Rust vs Design-App Canvas

This document is the authoritative reference for translating Rust WASM direction indices to the design-app flat-top canvas direction indices. Any TypeScript file that consumes flow_out, boundary_dir, or any other Rust-emitted direction value must route through rustDirToFlatTopDir from hexCanvas.ts rather than hard-coding its own table.


1. Rust Convention (mc-core/algorithms/hex.rs)

Coordinate system: axial (q, r) with s = -q - r (cube constraint).

Storage grid: odd-q offset, where col = q and row = r + (q (q & 1)) / 2. Odd columns are shifted up in axial row relative to even columns.

Direction indices 05 (AXIAL_DIRECTIONS):

Index Name Axial delta (dq, dr)
0 E (+1, 0)
1 NE (+1, 1)
2 NW ( 0, 1)
3 W (1, 0)
4 SW (1, +1)
5 SE ( 0, +1)

These labels (E/W as neighbours) are the pointy-top convention. They match HexUtils.AXIAL_DIRECTIONS in GDScript — no translation needed on the Godot side.

ODD_Q_NEIGHBORS gives the same neighbours expressed as offset (dcol, drow):

Parity E NE NW W SW SE
even col (+1, 0) (+1, 1) (0, 1) (1, 1) (1, 0) (0, +1)
odd col (+1, +1) (+1, 0) (0, 1) (1, 0) (1, +1) (0, +1)

Note the parity dependence: W is (1, 1) from an even column but (1, 0) from an odd column. This is the fundamental source of parity bugs.


2. Design-App Canvas Convention (hexCanvas.ts)

Orientation: flat-top. Corners are placed at angles 0°, 60°, 120°, 180°, 240°, 300° (E and W positions are corners, not edge midpoints).

Pixel layout: hexToPixel places odd columns shifted down by h/2 relative to even columns (where h = size * √3).

hexCorners corner indices (clockwise from E):

Index Position
0 E
1 SE
2 SW
3 W
4 NW
5 NE

EDGE_CORNERS — six edges (flat-top has no E or W edges):

Index Name Corner pair
0 NE 5→0 (NE→E)
1 SE 0→1 (E→SE)
2 S 1→2 (SE→SW)
3 SW 2→3 (SW→W)
4 NW 3→4 (W→NW)
5 N 4→5 (NW→NE)

neighborCoords(col, row) — neighbor offsets:

Index Name Even col (dcol, drow) Odd col (dcol, drow)
0 NE (+1, 1) (+1, 0)
1 SE (+1, 0) (+1, +1)
2 S (0, +1) (0, +1)
3 SW (1, 0) (1, +1)
4 NW (1, 1) (1, 0)
5 N (0, 1) (0, 1)

Opposite directions: 0↔3 (NE↔SW), 1↔4 (SE↔NW), 2↔5 (S↔N).


3. Translation Table — Rust → Flat-Top

The translation is parity-independent: the axial→offset→flat-top path produces the same flat-top index for both even and odd columns.

Rust dir Rust name Flat-top dir Flat-top name
0 E 1 SE
1 NE 0 NE
2 NW 5 N
3 W 4 NW
4 SW 3 SW
5 SE 2 S

Why parity-independent? The parity shift in Rust's odd-q convention moves odd-col axial rows in the same direction as the flat-top canvas's odd-col pixel shift. The asymmetry cancels: the physical hex each Rust direction points to is always the same flat-top named direction, regardless of which column you start in.


4. Worked Example — flow_out = 1 (Rust NE)

Scenario: tile at (col=4, row=3) (even column). flow_out = 1 → Rust NE.

  1. Look up translation: Rust 1 (NE) → Flat-top 0 (NE).
  2. neighborCoords(4, 3)[0] = (4+1, 31) = (5, 2).
  3. The flow arrow points from (4,3) toward the center of tile (5, 2).

Same tile at (col=5, row=3) (odd column). flow_out = 1 → still Flat-top 0 (NE). 4. neighborCoords(5, 3)[0] = (5+1, 3+0) = (6, 3). 5. The flow arrow points toward (6, 3) — the correct NE neighbor of an odd column.

The same Rust index produces correct output for both parities via rustDirToFlatTopDir(1, col) regardless of col.


5. Implementation

// hexCanvas.ts
export function rustDirToFlatTopDir(rustDir: number, col: number): number;

The col parameter is accepted for documentation clarity but the mapping is parity-independent. The function throws RangeError for rustDir outside 05.

Verified by 12-case Vitest suite in .project/designs/app/src/utils/worldGen/hexCanvas.test.ts.


6. Non-goals

  • Changing Rust's AXIAL_DIRECTIONS — they are locked and match GDScript.
  • Changing hexCorners orientation — flat-top is the canvas baseline.
  • Godot translation — Rust's pointy-top labels match GDScript's HexUtils, so no bridge-crossing translation is needed on the Godot side.