4.9 KiB
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 0–5 (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.
- Look up translation: Rust 1 (NE) → Flat-top 0 (NE).
neighborCoords(4, 3)[0]=(4+1, 3−1)=(5, 2).- 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 0–5.
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
hexCornersorientation — 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.