An engine-agnostic hex-map terrain/feature editor. Zero build, vanilla JavaScript + Canvas2D.
Load a hex map, assign each hex's terrain, its in-hex features, and its multi-feature hexsides by hand,
and export the canonical hexgrid / terrain / hexsides JSON your game reads.
- What it is
- Why
- Quick start
- Features
- Edge paint quick use
- Data model
- Bring your own game
- WMP pipeline
- TWU pipeline (rivers + rail)
- Testing
- License
Open a map, see its hex grid, click a hex. Set the hex's base terrain, toggle in-hex features (city, town, fort, port, objective...), and set what each of its six edges carries. Hexsides hold several features at once, so a single edge can be both a river and a road. Edges are shared, so assigning one also assigns it for the neighbor. Export the result as the JSON files your engine loads.
It is the deliberate, human-in-the-loop alternative to auto-detecting this from a scan. You already know where the rivers and ridges go; Hexwright lets you say so quickly, and check your work at a glance.
Auto-snapping hand-traced features to hexsides is brittle: a meandering printed river sits well off the clean geometric edges, so proximity matching fails. A human-in-the-loop editor is what real wargame studios use. It is deterministic, correct, and reusable, and it doubles as the correction surface for any project whose auto-digitized terrain needs fixing. When a classifier can produce a rough first pass (see the WMP pipeline), Hexwright imports it as an editable draft you refine.
Zero build, no runtime dependencies. From the repo root:
npm run serve # or: python3 -m http.server 8000
# open http://localhost:8000 -> "Load GotA sample"
The sample loader needs the local server for fetch; the file pickers work either way.
-
Base terrain per hex, from a configurable palette.
-
In-hex features (city, town, fort, port, airfield, objective, resource...), multiple per hex.
-
Multi-feature hexsides — each edge can carry several features at once (a stream and a road). They render distinctly: edge features (river, ridge, cliff) run as lines along the edge, while crossings (road, rail, bridge) draw as short rungs across the edge midpoint, so a road that crosses a river reads at a glance. The inspector groups the two kinds accordingly.
-
View modes — Map (scan + reference traces), Classification (data only, no photo), Both.
-
Overlay export — render the current classification to a PNG at the source-map resolution, so you can drop it beside the scan and spot mistakes.
-
Anomaly check — highlight unclassified hexes, orphan hexsides, and unconfirmed draft hexes.
-
Configurable palette — terrain, features, hexside features, kinds, and colors live in a JSON config; any game supplies its own vocabulary. The bundled default is GotA's (
palettes/gota.json). -
WMP draft import — ingest a wargame-map-parser classification as a low-confidence draft to refine.
-
TWU on-ramp — import TWU
rivers.json(hexsidespair-arrays) andrail.json(linkspair-arrays or{a,b}objects) with strict validation and loud errors, then export TWU-exact files (rivers.json+rail.jsonwithhexesendpoint union). -
Reference trace overlays with opacity control, brush drag-assign, edge-paint drag-assign, undo/redo, and a palette-driven legend.
-
Session autosave — your work persists to
localStorageon every change and restores on the next visit, so an accidental reload never loses hand-assignment work. -
Overlay opacity slider — fade the terrain-fill wash in Both view so the scan stays traceable underneath; hexsides, grid, and traces keep full strength. Dragging it from any view switches you to Both (it never silently no-ops).
-
Nudge map alignment (
n) — drag the scan under the fixed digital grid, or arrow keys for 1-px steps (Shift = ×10). The offset autosaves with the project: align once, aligned forever. The pragmatic answer to calibration drift — move the imagery, keep the hex data canonical. -
In-app help (
?) — full guide + shortcut table from the toolbar. -
Double-click launcher (
Launch Hexwright.command, macOS) — starts the local server (from the PARENT folder, so gitignoredlocal/manifests can reference sibling-repo full-resolution maps that must not enter this public repo) and opens the editor; boots straight intolocal/gota-fullres.jsonwhen present via the?project=<manifest>URL parameter.
Turn on Edge paint (e), pick a hexside feature chip, then paint directly on map edges.
- Click toggles the active feature on the nearest valid shared edge.
- Drag paints (set-on) across edges; one drag stroke is one undo entry.
- Alt+click / Alt+drag erases the active feature from hit edges.
- Edge paint takes pointer priority while enabled; turn it off and normal pan/inspect behavior returns.
| Shortcut | Action |
|---|---|
Drag |
Pan |
Wheel |
Zoom |
Click |
Inspect hex |
e |
Toggle edge paint mode |
b |
Toggle brush mode |
v |
Cycle view mode |
1-0 |
Quick terrain select |
Esc |
Close inspector |
Ctrl/Cmd + Z |
Undo |
A project is flat-top even-q, addressed by CCRR hex codes ("0803" = column 08, row 03).
hexgrid— grid calibration (the hex-to-pixel formula + image dimensions).terrain—{"terrain": {"CCRR": terrainKey}}; one base terrain per hex.hexsides(exported) — grouped by layer for back-compatibility:{"rivers":[{a,b}], "roads":[...], "mountains":[...], ...}, each shared edge stored once witha<b. An edge carrying several features appears in each of its layers. Untouched layers such astheatersandboundariessurvive a load/export round-trip verbatim.hexFeatures—{"CCRR": ["city", ...]}in-hex point features.
Internally hexsides are per-edge feature arrays; the grouped shape is the export contract.
Hexwright is data-contract driven. To onboard a new game you provide a grid JSON, a palette JSON, and a project manifest.
- Grid contract (
hexgrid): include the flat-top even-q calibration fields Hexwright reads (x_model.x_intercept_col0,x_model.col_pitch_x,y_model.y_intercept_row0,y_model.row_pitch_y,y_model.even_col_down_offset, or equivalent legacy aliases) plusimage_fullwhen available. - Palette contract: define
terrain,hexFeatures, andhexsideFeaturesin JSON. Each hexside feature needskey,kind(edgeorcrossing), color, and optionalexportLayer/aliases. - Manifest contract:
name,map,hexgrid,terrain, optionalhexsides, optionaltraces, optionalimageFull, optionalpalette.
Manifest example:
{
"name": "My Game",
"map": "assets/board-web.jpg",
"imageFull": [5000, 3200],
"hexgrid": "data/hexgrid.json",
"terrain": "data/terrain.json",
"hexsides": "data/hexsides.json",
"palette": "palettes/my-game.json"
}Load a manifest directly at boot with ?project=<manifest-url>, for example:
wargame-map-parser can classify hex-fill terrain from a scan. Its output already uses the same CCRR addressing:
python -m parser.export_hexwright wmp-terrain.json -o gota-terrain.hexwright.json
In Hexwright, Import WMP draft loads that file, marks every imported hex as an unconfirmed draft (visible in the Anomaly overlay), and you refine + confirm from there. The full loop: scan -> WMP rough classify -> Hexwright hand-refine -> canonical export.
TWU on-ramp scope is intentionally narrow: rivers + rail only (no fortress import/export path).
-
Keep
hexwrightandtwu-deluxe-digitalas sibling checkouts. -
Start the server from the parent directory so sibling paths resolve:
cd .. python3 -m http.server 8000 -
Open Hexwright with the TWU template manifest:
http://localhost:8000/hexwright/?project=samples/twu-project.json -
Update
samples/twu-project.jsonpaths to match your local TWU repo layout. -
Use Import TWU layer for each source file (
rivers.json, thenrail.json). -
Refine by hand with edge paint.
-
Use Export -> TWU rivers+rail to write:
rivers.json:_comment+hexsides: [["a","b"], ...]rail.json:_comment+links: [["a","b"], ...]+hexesendpoint union
Validation is strict and loud: wrong-shaped TWU files fail import with an explicit status error and no data changes.
npm test # headless Playwright suites: load / assign / export / round-trip / UI / autosave
MIT © Ray Weiss
