mapwright 0.12.0__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {mapwright-0.12.0 → mapwright-0.17.0}/.gitignore +1 -0
  2. {mapwright-0.12.0 → mapwright-0.17.0}/CHANGELOG.md +79 -1
  3. {mapwright-0.12.0 → mapwright-0.17.0}/NOTICE +20 -1
  4. {mapwright-0.12.0 → mapwright-0.17.0}/PKG-INFO +124 -5
  5. {mapwright-0.12.0 → mapwright-0.17.0}/README.md +120 -4
  6. mapwright-0.17.0/docs/gallery/age-old.png +0 -0
  7. mapwright-0.17.0/docs/gallery/age-old.svg +1 -0
  8. mapwright-0.17.0/docs/gallery/age-young.png +0 -0
  9. mapwright-0.17.0/docs/gallery/age-young.svg +1 -0
  10. mapwright-0.17.0/docs/gallery/archipelago.png +0 -0
  11. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/archipelago.svg +1 -1
  12. mapwright-0.17.0/docs/gallery/arctic.png +0 -0
  13. mapwright-0.17.0/docs/gallery/arctic.svg +1 -0
  14. mapwright-0.17.0/docs/gallery/atlas.png +0 -0
  15. mapwright-0.17.0/docs/gallery/atlas_pack/README.md +17 -0
  16. mapwright-0.17.0/docs/gallery/atlas_pack/city_castle_1.png +0 -0
  17. mapwright-0.17.0/docs/gallery/atlas_pack/city_large_1.png +0 -0
  18. mapwright-0.17.0/docs/gallery/atlas_pack/city_town_1.png +0 -0
  19. mapwright-0.17.0/docs/gallery/atlas_pack/city_village_1.png +0 -0
  20. mapwright-0.17.0/docs/gallery/atlas_pack/decoration_compass_1.png +0 -0
  21. mapwright-0.17.0/docs/gallery/atlas_pack/decoration_creature_1.png +0 -0
  22. mapwright-0.17.0/docs/gallery/atlas_pack/decoration_ship_1.png +0 -0
  23. mapwright-0.17.0/docs/gallery/atlas_pack/dune_1.png +0 -0
  24. mapwright-0.17.0/docs/gallery/atlas_pack/hill_1.png +0 -0
  25. mapwright-0.17.0/docs/gallery/atlas_pack/hill_2.png +0 -0
  26. mapwright-0.17.0/docs/gallery/atlas_pack/manifest.json +122 -0
  27. mapwright-0.17.0/docs/gallery/atlas_pack/mountain_mid_1.png +0 -0
  28. mapwright-0.17.0/docs/gallery/atlas_pack/mountain_old_1.png +0 -0
  29. mapwright-0.17.0/docs/gallery/atlas_pack/mountain_old_2.png +0 -0
  30. mapwright-0.17.0/docs/gallery/atlas_pack/mountain_young_1.png +0 -0
  31. mapwright-0.17.0/docs/gallery/atlas_pack/mountain_young_2.png +0 -0
  32. mapwright-0.17.0/docs/gallery/atlas_pack/tree_cactus_1.png +0 -0
  33. mapwright-0.17.0/docs/gallery/atlas_pack/tree_deciduous_1.png +0 -0
  34. mapwright-0.17.0/docs/gallery/atlas_pack/tree_deciduous_2.png +0 -0
  35. mapwright-0.17.0/docs/gallery/atlas_pack/tree_pine_1.png +0 -0
  36. mapwright-0.17.0/docs/gallery/atlas_pack/tree_pine_2.png +0 -0
  37. mapwright-0.17.0/docs/gallery/continent.png +0 -0
  38. mapwright-0.17.0/docs/gallery/continent.svg +1 -0
  39. mapwright-0.17.0/docs/gallery/desert.png +0 -0
  40. mapwright-0.17.0/docs/gallery/desert.svg +1 -0
  41. mapwright-0.17.0/docs/gallery/highlands.png +0 -0
  42. mapwright-0.17.0/docs/gallery/highlands.svg +1 -0
  43. mapwright-0.17.0/docs/gallery/islands.png +0 -0
  44. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/islands.svg +1 -1
  45. mapwright-0.17.0/docs/gallery/pangaea.png +0 -0
  46. mapwright-0.17.0/docs/gallery/pangaea.svg +1 -0
  47. mapwright-0.17.0/docs/gallery/regions.png +0 -0
  48. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/regions.svg +1 -1
  49. mapwright-0.17.0/docs/gallery/roads.png +0 -0
  50. mapwright-0.17.0/docs/gallery/roads.svg +1 -0
  51. mapwright-0.17.0/docs/gallery/template-atoll.png +0 -0
  52. mapwright-0.17.0/docs/gallery/template-atoll.svg +1 -0
  53. mapwright-0.17.0/docs/gallery/template-isthmus.png +0 -0
  54. mapwright-0.17.0/docs/gallery/template-isthmus.svg +1 -0
  55. mapwright-0.17.0/docs/gallery/theme-blueprint.png +0 -0
  56. mapwright-0.17.0/docs/gallery/theme-blueprint.svg +1 -0
  57. mapwright-0.17.0/docs/gallery/theme-dune.png +0 -0
  58. mapwright-0.17.0/docs/gallery/theme-dune.svg +1 -0
  59. mapwright-0.17.0/docs/gallery/theme-neon.png +0 -0
  60. mapwright-0.17.0/docs/gallery/theme-neon.svg +1 -0
  61. mapwright-0.17.0/docs/gallery/theme-parchment.png +0 -0
  62. mapwright-0.12.0/docs/gallery/roads.svg → mapwright-0.17.0/docs/gallery/theme-parchment.svg +1 -1
  63. mapwright-0.17.0/docs/gallery/tropical.png +0 -0
  64. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/tropical.svg +1 -1
  65. {mapwright-0.12.0 → mapwright-0.17.0}/examples/gallery.py +82 -0
  66. {mapwright-0.12.0 → mapwright-0.17.0}/pyproject.toml +5 -2
  67. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/__init__.py +9 -1
  68. mapwright-0.17.0/src/mapwright/atlas_renderer.py +359 -0
  69. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/config.py +4 -0
  70. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/svg_renderer.py +28 -45
  71. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/terrain.py +281 -73
  72. mapwright-0.17.0/src/mapwright/themes.py +177 -0
  73. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_api_contract.py +5 -0
  74. mapwright-0.17.0/tests/test_atlas_renderer.py +163 -0
  75. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_terrain.py +60 -0
  76. mapwright-0.17.0/tests/test_themes.py +100 -0
  77. mapwright-0.12.0/docs/gallery/archipelago.png +0 -0
  78. mapwright-0.12.0/docs/gallery/arctic.png +0 -0
  79. mapwright-0.12.0/docs/gallery/arctic.svg +0 -1
  80. mapwright-0.12.0/docs/gallery/continent.png +0 -0
  81. mapwright-0.12.0/docs/gallery/continent.svg +0 -1
  82. mapwright-0.12.0/docs/gallery/desert.png +0 -0
  83. mapwright-0.12.0/docs/gallery/desert.svg +0 -1
  84. mapwright-0.12.0/docs/gallery/highlands.png +0 -0
  85. mapwright-0.12.0/docs/gallery/highlands.svg +0 -1
  86. mapwright-0.12.0/docs/gallery/islands.png +0 -0
  87. mapwright-0.12.0/docs/gallery/pangaea.png +0 -0
  88. mapwright-0.12.0/docs/gallery/pangaea.svg +0 -1
  89. mapwright-0.12.0/docs/gallery/regions.png +0 -0
  90. mapwright-0.12.0/docs/gallery/roads.png +0 -0
  91. mapwright-0.12.0/docs/gallery/tropical.png +0 -0
  92. {mapwright-0.12.0 → mapwright-0.17.0}/.github/workflows/ci.yml +0 -0
  93. {mapwright-0.12.0 → mapwright-0.17.0}/.github/workflows/publish.yml +0 -0
  94. {mapwright-0.12.0 → mapwright-0.17.0}/LICENSE +0 -0
  95. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/citadel.png +0 -0
  96. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/citadel.svg +0 -0
  97. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/dungeon.png +0 -0
  98. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/dungeon.svg +0 -0
  99. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/port.png +0 -0
  100. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/port.svg +0 -0
  101. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/town.png +0 -0
  102. {mapwright-0.12.0 → mapwright-0.17.0}/docs/gallery/town.svg +0 -0
  103. {mapwright-0.12.0 → mapwright-0.17.0}/examples/benchmark.py +0 -0
  104. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/_geometry.py +0 -0
  105. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/_graph.py +0 -0
  106. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/_serde.py +0 -0
  107. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/dungeon.py +0 -0
  108. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/dungeon_renderer.py +0 -0
  109. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/names.py +0 -0
  110. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/regions.py +0 -0
  111. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/rng.py +0 -0
  112. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/roads.py +0 -0
  113. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/settlement.py +0 -0
  114. {mapwright-0.12.0 → mapwright-0.17.0}/src/mapwright/settlement_renderer.py +0 -0
  115. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_config.py +0 -0
  116. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_dungeon.py +0 -0
  117. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_dungeon_renderer.py +0 -0
  118. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_geometry.py +0 -0
  119. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_graph.py +0 -0
  120. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_names.py +0 -0
  121. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_properties.py +0 -0
  122. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_regions.py +0 -0
  123. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_rng.py +0 -0
  124. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_roads.py +0 -0
  125. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_serialize.py +0 -0
  126. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_settlement.py +0 -0
  127. {mapwright-0.12.0 → mapwright-0.17.0}/tests/test_svg_renderer.py +0 -0
@@ -28,3 +28,4 @@ htmlcov/
28
28
 
29
29
  # Local working notes (roadmap, scratch)
30
30
  *.local.md
31
+ memory/
@@ -8,6 +8,81 @@ All notable changes to mapwright are documented here. The format follows
8
8
  `tests/test_api_contract.py`). While the version is `0.x`, minor versions may
9
9
  make breaking changes; these will always be noted here.
10
10
 
11
+ ## [0.17.0] — 2026-06-02
12
+
13
+ ### Added
14
+ - **Render themes — `Theme` + `THEMES`.** `RegionalSVGRenderer` now takes a
15
+ `theme=` (a name or a `Theme`): a palette plus an optional biome *vocabulary*
16
+ that re-skins the same neutral terrain without regenerating anything. The
17
+ `Biome` enum is unchanged — a theme only decides how each biome looks and is
18
+ named — so this is purely additive and the contract is stable. Built-ins:
19
+ `parchment` (default, **byte-identical** to the previous output), `neon`
20
+ (Tron / digital-grid), `dune` (Tatooine / sand medium), and `blueprint`.
21
+ `Theme` is plain hex-string data (JSON-friendly), so a host or image service
22
+ can author new ones; `Theme.biome_label()` exposes the vocabulary (e.g.
23
+ `OCEAN` → "Void"). This is the first slice of the "Dominant Medium" /
24
+ render-theme direction — pair a theme with a matching `ArtPack` for a full
25
+ restyle. Gallery gains a same-continent neon/dune/blueprint showcase.
26
+
27
+ ## [0.16.0] — 2026-06-02
28
+
29
+ ### Added
30
+ - **`AtlasRenderer` + `ArtPack` — hand-drawn / themed atlas rendering.** A new
31
+ optional renderer that stamps symbol images from an external *art pack* onto a
32
+ `TerrainResult` to produce a hand-drawn (or any-style) fantasy-map look:
33
+ mountains (young/mid/old by `land_age`), hills, forests (pine/deciduous/cactus
34
+ by climate), dunes, settlements (by marker kind), and sea decorations + a
35
+ compass rose. mapwright ships **no art** — an art pack is a directory of PNG
36
+ symbols plus an optional `manifest.json` that maps mapwright's neutral concepts
37
+ (`Biome`, `land_age`, settlement size) onto art "slots"; a host (e.g. an
38
+ image-generation service) produces packs in any style and this renderer just
39
+ places them. `ArtPack.from_directory()` reads a manifest, or auto-discovers
40
+ slots from a conventional (Nortantis-style) folder layout. Missing fine-grained
41
+ slots fall back to a coarser sibling, so partial packs still work. Requires
42
+ Pillow — install the optional extra: `pip install "mapwright[atlas]"`. The
43
+ core library stays numpy-only; without Pillow, `import mapwright` is unaffected
44
+ and only `AtlasRenderer` rendering raises a clear install hint.
45
+
46
+ ## [0.15.0] — 2026-06-02
47
+
48
+ ### Added
49
+ - **`land_age` — geological age of the terrain** (a mapwright-original idea). A new
50
+ `WorldMapConfig` knob: 0 = *young* (jagged, tall, snow-capped peaks — think the
51
+ Rockies), 1 = *old* (worn down to rounded hills and lowlands — the Appalachians).
52
+ It shapes the hypsometric curve (a gamma on land elevation → more/fewer mountains)
53
+ and, for old land, applies weathering passes that smooth the relief. The default
54
+ (0.5) is neutral — terrain is byte-identical to before, so the feature is purely
55
+ opt-in. First slice of a broader age/era/wealth axis (forests, settlements next).
56
+
57
+ ## [0.14.0] — 2026-06-02
58
+
59
+ ### Added
60
+ - **Heightmap templates** — an optional, controllable alternative to the default
61
+ tectonic auto-generation. `RegionalTerrainGenerator.generate(..., template=…)`
62
+ builds the heightmap from composable elevation ops (hill, pit, range, trough,
63
+ strait) spread over the cell graph, and `TERRAIN_TEMPLATES` provides named
64
+ continent archetypes: `continents`, `archipelago`, `peninsula`, `isthmus`,
65
+ `volcano`, `atoll`. A template sets the *pattern* of high/low ground; `config`
66
+ still drives sea level (percentile), climate, and rivers on top. Clean-room from
67
+ the documented idea in Azgaar's Fantasy-Map-Generator (see NOTICE). The default
68
+ (no template) tectonic terrain is byte-identical — this is purely additive.
69
+
70
+ ## [0.13.0] — 2026-06-02
71
+
72
+ ### Changed
73
+ - **Tectonic-plate terrain.** The heightmap is now built from a simple plate
74
+ simulation instead of a radial/noise field: the map is tiled into continental
75
+ and oceanic plates (Voronoi over plate seeds) that drift, and **convergent plate
76
+ boundaries raise mountain ranges** — so continents get organic coastlines *and*
77
+ believable linear mountain belts, with no centre bias. The `continents` knob is
78
+ the number of continental plates; oceanic plates interleave between them, so
79
+ multi-continent worlds (`archipelago`, `islands`) fragment into scattered islands
80
+ around an inner sea rather than one blob. Sea level is now **percentile-based**
81
+ (`sea_level` maps directly to the water fraction). Rivers form reliably across
82
+ all presets (a flux-quantile source threshold). Clean-room from the documented
83
+ ideas of Nortantis (tectonics) and the Fractal Worldmap Generator (percentile
84
+ sea level); see NOTICE. Regenerated the terrain/roads/regions gallery.
85
+
11
86
  ## [0.12.0] — 2026-06-02
12
87
 
13
88
  ### Changed
@@ -198,7 +273,10 @@ Initial release. Domain-neutral procedural fantasy map & world generation.
198
273
  polygons, coastline, rivers, labelled markers. `compute_cell_polygons` rebuilds
199
274
  convex Voronoi polygons via half-plane clipping.
200
275
 
201
- [Unreleased]: https://github.com/sligara7/mapwright/compare/v0.12.0...HEAD
276
+ [Unreleased]: https://github.com/sligara7/mapwright/compare/v0.15.0...HEAD
277
+ [0.15.0]: https://github.com/sligara7/mapwright/compare/v0.14.0...v0.15.0
278
+ [0.14.0]: https://github.com/sligara7/mapwright/compare/v0.13.0...v0.14.0
279
+ [0.13.0]: https://github.com/sligara7/mapwright/compare/v0.12.0...v0.13.0
202
280
  [0.12.0]: https://github.com/sligara7/mapwright/compare/v0.11.0...v0.12.0
203
281
  [0.11.0]: https://github.com/sligara7/mapwright/compare/v0.10.0...v0.11.0
204
282
  [0.10.0]: https://github.com/sligara7/mapwright/compare/v0.9.0...v0.10.0
@@ -14,7 +14,9 @@ copied from them; this NOTICE credits the lineage of the techniques.
14
14
  https://github.com/Azgaar/Fantasy-Map-Generator
15
15
  Ideas: jittered-grid Voronoi with Lloyd relaxation, flux-accumulation
16
16
  rivers, temperature x precipitation -> biome matrix, Markov per-culture
17
- name generation, single-seed determinism, SVG cartographic rendering.
17
+ name generation, single-seed determinism, SVG cartographic rendering,
18
+ composable heightmap templates (hill/range/pit/trough/strait ops combined
19
+ into named continent archetypes).
18
20
 
19
21
  * "Generating fantasy maps" by Martin O'Leary (mewo2), and its C++ implementation
20
22
  FantasyMapGenerator by Ryan L. Guy (rlguy) (Zlib License)
@@ -31,5 +33,22 @@ copied from them; this NOTICE credits the lineage of the techniques.
31
33
  subdivided into Voronoi wards. mapwright's settlement code is an independent
32
34
  clean-room implementation built on its own geometry primitives.
33
35
 
36
+ * Nortantis by Joseph Heydorn (AGPLv3)
37
+ https://github.com/jeheydorn/nortantis
38
+ Concept only (NO code derived — AGPLv3 is incompatible with this MIT
39
+ project): the idea of shaping land with a simple tectonic-plate simulation
40
+ (continental/oceanic plates whose convergent boundaries raise mountain
41
+ ranges), and the general approach of compositing a map from stamped symbol
42
+ images. mapwright's plate model and AtlasRenderer are independent clean-room
43
+ implementations; AtlasRenderer's optional art-pack auto-discovery merely
44
+ recognises a Nortantis-like folder naming convention (mountains/, hills/,
45
+ trees/, …) as a data layout — no Nortantis code or art is used or bundled.
46
+
47
+ * "Fractal Worldmap Generator" by John Olsson / Torben Æ. Mogensen
48
+ https://www.lysator.liu.se/~johol/fwmg/fwmg.html
49
+ Ideas (clean-room): a percentile/histogram sea level so a target water
50
+ fraction is met, and elevation built from many random "faults" rather than a
51
+ centred radial field.
52
+
34
53
  The name lists ("namebases") bundled with mapwright are original, hand-authored
35
54
  data, not derived from any third-party generator.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapwright
3
- Version: 0.12.0
3
+ Version: 0.17.0
4
4
  Summary: Domain-neutral procedural fantasy map & world generation: Voronoi terrain, hydraulic erosion, biomes, rivers, Markov place-names, and shaded-relief SVG.
5
5
  Project-URL: Homepage, https://github.com/sligara7/mapwright
6
6
  Project-URL: Repository, https://github.com/sligara7/mapwright
@@ -16,8 +16,11 @@ Classifier: Topic :: Games/Entertainment :: Role-Playing
16
16
  Classifier: Topic :: Multimedia :: Graphics
17
17
  Requires-Python: >=3.10
18
18
  Requires-Dist: numpy>=1.26
19
+ Provides-Extra: atlas
20
+ Requires-Dist: pillow>=10; extra == 'atlas'
19
21
  Provides-Extra: dev
20
22
  Requires-Dist: hypothesis>=6; extra == 'dev'
23
+ Requires-Dist: pillow>=10; extra == 'dev'
21
24
  Requires-Dist: pytest>=7.4; extra == 'dev'
22
25
  Requires-Dist: ruff>=0.1; extra == 'dev'
23
26
  Description-Content-Type: text/markdown
@@ -38,7 +41,27 @@ own tiles/entities however you like.
38
41
 
39
42
  ## Gallery
40
43
 
41
- Every image below is a deterministic render of a built-in preset (or a dungeon),
44
+ **`AtlasRenderer`** the same neutral terrain, skinned with a hand-drawn *art pack*.
45
+ The art here is original, generated through mapwright's companion image service and
46
+ stamped where the physics put it (mountains on the ranges, forests by climate, sea
47
+ serpents offshore). mapwright itself ships no art — the pack is the skin:
48
+
49
+ <p align="center">
50
+ <img width="640" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/atlas.png" alt="hand-drawn atlas rendered from a sample art pack">
51
+ </p>
52
+
53
+ **Render themes** — the *same* continent (same cells, rivers, roads, settlements),
54
+ re-skinned by swapping a `Theme` (palette + biome vocabulary). No regeneration:
55
+
56
+ <table>
57
+ <tr>
58
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/theme-neon.png" alt="neon (Tron) theme"><br><sub><code>theme="neon"</code></sub></td>
59
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/theme-dune.png" alt="dune (sand) theme"><br><sub><code>theme="dune"</code></sub></td>
60
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/theme-blueprint.png" alt="blueprint theme"><br><sub><code>theme="blueprint"</code></sub></td>
61
+ </tr>
62
+ </table>
63
+
64
+ Below: deterministic shaded-relief renders of each built-in preset (or a dungeon),
42
65
  produced by [`examples/gallery.py`](examples/gallery.py):
43
66
 
44
67
  <table>
@@ -65,10 +88,19 @@ produced by [`examples/gallery.py`](examples/gallery.py):
65
88
  <tr>
66
89
  <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/roads.png" alt="settlements linked by terrain-routed roads"><br><sub><code>RegionalRoadGenerator</code></sub></td>
67
90
  <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/regions.png" alt="land partitioned into named territories"><br><sub><code>RegionGenerator</code></sub></td>
68
- <td></td>
91
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/template-isthmus.png" alt="isthmus heightmap template"><br><sub><code>template="isthmus"</code></sub></td>
92
+ </tr>
93
+ <tr>
94
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/template-atoll.png" alt="atoll heightmap template"><br><sub><code>template="atoll"</code></sub></td>
95
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/age-young.png" alt="young jagged terrain"><br><sub><code>land_age=0</code> (young)</sub></td>
96
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/age-old.png" alt="old worn terrain"><br><sub><code>land_age=1</code> (old)</sub></td>
69
97
  </tr>
70
98
  </table>
71
99
 
100
+ The two right-hand maps above are the **same continent** at `land_age=0` (young, jagged,
101
+ snow-capped peaks) vs `land_age=1` (old, worn down to rounded hills) — a mapwright-original
102
+ "geological age" knob.
103
+
72
104
  Regenerate them with `python examples/gallery.py` (SVGs always; PNGs when
73
105
  `cairosvg` is installed).
74
106
 
@@ -76,6 +108,8 @@ Regenerate them with `python examples/gallery.py` (SVGs always; PNGs when
76
108
 
77
109
  ```bash
78
110
  pip install mapwright
111
+ # hand-drawn / themed atlas rendering (adds Pillow):
112
+ pip install "mapwright[atlas]"
79
113
  # latest from git:
80
114
  pip install git+https://github.com/sligara7/mapwright.git
81
115
  # or, for local development:
@@ -112,6 +146,18 @@ WorldMapConfig.from_dict({"temperature": 5, "continents": -3}) # -> safe, clamp
112
146
  Presets: `continent`, `pangaea`, `archipelago`, `islands`, `highlands`, `desert`,
113
147
  `arctic`, `tropical`.
114
148
 
149
+ Terrain defaults to a **tectonic-plate** simulation (organic coasts + mountain ranges).
150
+ For a controllable continent *archetype*, pass a `template` (Azgaar-style composed
151
+ heightmap ops) — `config` still drives sea level, climate, and rivers on top of it:
152
+
153
+ ```python
154
+ from mapwright import RegionalTerrainGenerator, SeededRNG, WorldMapConfig, TERRAIN_TEMPLATES
155
+
156
+ print(list(TERRAIN_TEMPLATES)) # archipelago, volcano, peninsula, isthmus, atoll, continents
157
+ world = RegionalTerrainGenerator(SeededRNG(5)).generate(
158
+ 80, 58, WorldMapConfig(sea_level=0.55), template="archipelago")
159
+ ```
160
+
115
161
  Save and reload worlds (and dungeons) — JSON round-trips losslessly, so a reloaded
116
162
  world renders byte-identically:
117
163
 
@@ -169,9 +215,11 @@ Settlement presets: `hamlet`, `village`, `town`, `city`, `port`, `citadel`.
169
215
  |-----------|--------------|
170
216
  | `SeededRNG` | One seed drives everything; `.derive(label)` yields independent, reproducible sub-streams (unifies stdlib + numpy). |
171
217
  | `NameGenerator` | Order-k character Markov names over hand-authored culture namebases; reproducible across processes. |
172
- | `RegionalTerrainGenerator` | Voronoi cells (Lloyd-relaxed) → noise-shaped **organic** land/sea mask + distance-to-coast elevation → Planchon–Darboux depression fill → flux + hydraulic/creep erosion → rivers + inland lakes → latitude/elevation climate with **rain-shadow** → Whittaker biomes. |
218
+ | `RegionalTerrainGenerator` | Voronoi cells (Lloyd-relaxed) → **tectonic-plate** heightmap (organic coasts + mountain ranges at plate collisions; percentile sea level) → Planchon–Darboux depression fill → flux + hydraulic/creep erosion → rivers + inland lakes → latitude/elevation climate with **rain-shadow** → Whittaker biomes. |
173
219
  | `compute_cell_polygons` | Reconstructs convex Voronoi polygons (half-plane clipping) for vector rendering. |
174
- | `RegionalSVGRenderer` | Shaded-relief (hillshade) SVG: biome polygons, coastline, rivers, roads, labelled markers. |
220
+ | `RegionalSVGRenderer` | Shaded-relief (hillshade) SVG: biome polygons, coastline, rivers, roads, labelled markers. Takes a `theme=`. |
221
+ | `Theme` / `THEMES` | A render palette + biome vocabulary; re-skins the same terrain (parchment / neon / dune / blueprint, or your own). The "Dominant Medium" layer. |
222
+ | `AtlasRenderer` / `ArtPack` | Hand-drawn / themed PNG: stamps symbols from an external *art pack* (mountains, forests, hills, settlements, sea decorations) onto the terrain. mapwright ships no art — a pack is a skin. Needs `pip install "mapwright[atlas]"`. |
175
223
  | `RegionalRoadGenerator` | Connects settlement sites with trade routes — an MST whose edges are A*-routed over the terrain (avoids sea, climbs/crosses rivers at a cost). |
176
224
  | `RegionGenerator` | Partitions land into named factions/territories: spread capitals seed a flood fill over the land graph (sea divides them); each `Region` is Markov-named. |
177
225
  | `DungeonGenerator` | BSP-partitioned rooms + minimum-spanning-tree corridors → rooms, corridor cells, and a walkable grid (with `Dungeon.ascii()`). |
@@ -182,6 +230,77 @@ Settlement presets: `hamlet`, `village`, `town`, `city`, `port`, `citadel`.
182
230
  Everything is neutral: `RegionalTerrainGenerator` returns a `TerrainResult` of `TerrainCell`s
183
231
  (each with a `Biome`), and you decide how a `Biome` maps to your world.
184
232
 
233
+ ## Atlas rendering & art packs
234
+
235
+ `RegionalSVGRenderer` draws a clean shaded-relief map. For a **hand-drawn** (or neon, or
236
+ scrap-metal, or any) look, `AtlasRenderer` stamps little symbol images — mountains, trees,
237
+ hills, towns, sea monsters, a compass — placed exactly where the physics put them.
238
+
239
+ mapwright bundles **no art**. The renderer is the *engine*; the art is a separate **art pack**
240
+ you point it at, so the same world can wear any style without re-generating anything:
241
+
242
+ ```python
243
+ from mapwright import SeededRNG, RegionalTerrainGenerator, ArtPack, AtlasRenderer, Marker
244
+
245
+ terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(80, 56)
246
+ markers = [Marker("Eldmoor", 40, 28, kind="settlement_castle")]
247
+
248
+ pack = ArtPack.from_directory("path/to/my-art-pack") # needs mapwright[atlas]
249
+ png = AtlasRenderer(pack, scale=12, seed=7).render(terrain, markers, land_age=0.3)
250
+ open("atlas.png", "wb").write(png)
251
+ ```
252
+
253
+ An **art pack** is just a directory of transparent PNG symbols plus an optional
254
+ `manifest.json` that maps mapwright's neutral concepts onto art **slots**:
255
+
256
+ ```jsonc
257
+ {
258
+ "name": "my-pack",
259
+ "colors": {"parchment": "#ecdfbf", "water": "#b5cad1",
260
+ "coast": "#463c2c", "label": "#2b2218"},
261
+ "slots": {
262
+ "mountain.young": {"files": ["mountains/sharp/*.png"], "width": 2.0, "anchor": "bottom"},
263
+ "mountain.old": {"files": ["mountains/eroded/*.png"]},
264
+ "hill": {"files": ["hills/*.png"]},
265
+ "tree.pine": {"files": ["trees/pine/*.png"]},
266
+ "tree.deciduous": {"files": ["trees/leafy/*.png"]},
267
+ "city.castle": {"files": ["cities/castle*.png"]},
268
+ "decoration.compass": {"files": ["compass/*.png"], "anchor": "center"}
269
+ }
270
+ }
271
+ ```
272
+
273
+ Slots the renderer asks for: terrain relief — `mountain.young` / `mountain.mid` /
274
+ `mountain.old` (chosen by `land_age`), `hill`, `tree.pine` / `tree.deciduous` /
275
+ `tree.cactus` (by climate), `dune`; settlements — `city.castle` / `city.large` /
276
+ `city.town` / `city.village` (by marker `kind`); decorations — `decoration.creature`
277
+ / `decoration.ship` / `decoration.compass`. A missing fine slot falls back to a coarser
278
+ sibling (`mountain.mid` → any `mountain.*`), so partial packs still render. With **no**
279
+ `manifest.json`, `ArtPack.from_directory()` auto-discovers slots from a conventional
280
+ folder layout. Because packs are pure data, a host like an image-generation service can
281
+ **produce them on demand** in any style — the generation stays the same; the pack is the skin.
282
+
283
+ ### Render themes
284
+
285
+ The vector `RegionalSVGRenderer` takes a **`Theme`** — a palette plus an optional biome
286
+ *vocabulary* — so the same neutral terrain re-skins into wildly different worlds without
287
+ regenerating anything. The neutral `Biome` enum never changes; a theme just decides how
288
+ each biome looks and is named:
289
+
290
+ ```python
291
+ from mapwright import RegionalSVGRenderer, THEMES
292
+
293
+ svg = RegionalSVGRenderer(theme="neon").render(terrain, markers, roads=roads)
294
+ # built-ins: "parchment" (default), "neon" (Tron/digital-grid), "dune" (sand), "blueprint"
295
+ THEMES["neon"].biome_label(Biome.OCEAN) # -> "Void" (the vocabulary layer)
296
+ ```
297
+
298
+ A `Theme` is plain hex-string data (JSON-friendly), so a host — or the same image service
299
+ that makes art packs — can author new ones. This is the "Dominant Medium" idea from
300
+ mapwright's longer-term vision: a sand planet, a digital grid, and an irradiated waste are
301
+ the *same map* wearing different skins. Pair a theme with a matching `ArtPack` for a full
302
+ restyle of both the vector and hand-drawn renders.
303
+
185
304
  ## Determinism
186
305
 
187
306
  Every generator draws from a `SeededRNG`. The same seed (and parameters) reproduces an
@@ -14,7 +14,27 @@ own tiles/entities however you like.
14
14
 
15
15
  ## Gallery
16
16
 
17
- Every image below is a deterministic render of a built-in preset (or a dungeon),
17
+ **`AtlasRenderer`** the same neutral terrain, skinned with a hand-drawn *art pack*.
18
+ The art here is original, generated through mapwright's companion image service and
19
+ stamped where the physics put it (mountains on the ranges, forests by climate, sea
20
+ serpents offshore). mapwright itself ships no art — the pack is the skin:
21
+
22
+ <p align="center">
23
+ <img width="640" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/atlas.png" alt="hand-drawn atlas rendered from a sample art pack">
24
+ </p>
25
+
26
+ **Render themes** — the *same* continent (same cells, rivers, roads, settlements),
27
+ re-skinned by swapping a `Theme` (palette + biome vocabulary). No regeneration:
28
+
29
+ <table>
30
+ <tr>
31
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/theme-neon.png" alt="neon (Tron) theme"><br><sub><code>theme="neon"</code></sub></td>
32
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/theme-dune.png" alt="dune (sand) theme"><br><sub><code>theme="dune"</code></sub></td>
33
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/theme-blueprint.png" alt="blueprint theme"><br><sub><code>theme="blueprint"</code></sub></td>
34
+ </tr>
35
+ </table>
36
+
37
+ Below: deterministic shaded-relief renders of each built-in preset (or a dungeon),
18
38
  produced by [`examples/gallery.py`](examples/gallery.py):
19
39
 
20
40
  <table>
@@ -41,10 +61,19 @@ produced by [`examples/gallery.py`](examples/gallery.py):
41
61
  <tr>
42
62
  <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/roads.png" alt="settlements linked by terrain-routed roads"><br><sub><code>RegionalRoadGenerator</code></sub></td>
43
63
  <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/regions.png" alt="land partitioned into named territories"><br><sub><code>RegionGenerator</code></sub></td>
44
- <td></td>
64
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/template-isthmus.png" alt="isthmus heightmap template"><br><sub><code>template="isthmus"</code></sub></td>
65
+ </tr>
66
+ <tr>
67
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/template-atoll.png" alt="atoll heightmap template"><br><sub><code>template="atoll"</code></sub></td>
68
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/age-young.png" alt="young jagged terrain"><br><sub><code>land_age=0</code> (young)</sub></td>
69
+ <td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/age-old.png" alt="old worn terrain"><br><sub><code>land_age=1</code> (old)</sub></td>
45
70
  </tr>
46
71
  </table>
47
72
 
73
+ The two right-hand maps above are the **same continent** at `land_age=0` (young, jagged,
74
+ snow-capped peaks) vs `land_age=1` (old, worn down to rounded hills) — a mapwright-original
75
+ "geological age" knob.
76
+
48
77
  Regenerate them with `python examples/gallery.py` (SVGs always; PNGs when
49
78
  `cairosvg` is installed).
50
79
 
@@ -52,6 +81,8 @@ Regenerate them with `python examples/gallery.py` (SVGs always; PNGs when
52
81
 
53
82
  ```bash
54
83
  pip install mapwright
84
+ # hand-drawn / themed atlas rendering (adds Pillow):
85
+ pip install "mapwright[atlas]"
55
86
  # latest from git:
56
87
  pip install git+https://github.com/sligara7/mapwright.git
57
88
  # or, for local development:
@@ -88,6 +119,18 @@ WorldMapConfig.from_dict({"temperature": 5, "continents": -3}) # -> safe, clamp
88
119
  Presets: `continent`, `pangaea`, `archipelago`, `islands`, `highlands`, `desert`,
89
120
  `arctic`, `tropical`.
90
121
 
122
+ Terrain defaults to a **tectonic-plate** simulation (organic coasts + mountain ranges).
123
+ For a controllable continent *archetype*, pass a `template` (Azgaar-style composed
124
+ heightmap ops) — `config` still drives sea level, climate, and rivers on top of it:
125
+
126
+ ```python
127
+ from mapwright import RegionalTerrainGenerator, SeededRNG, WorldMapConfig, TERRAIN_TEMPLATES
128
+
129
+ print(list(TERRAIN_TEMPLATES)) # archipelago, volcano, peninsula, isthmus, atoll, continents
130
+ world = RegionalTerrainGenerator(SeededRNG(5)).generate(
131
+ 80, 58, WorldMapConfig(sea_level=0.55), template="archipelago")
132
+ ```
133
+
91
134
  Save and reload worlds (and dungeons) — JSON round-trips losslessly, so a reloaded
92
135
  world renders byte-identically:
93
136
 
@@ -145,9 +188,11 @@ Settlement presets: `hamlet`, `village`, `town`, `city`, `port`, `citadel`.
145
188
  |-----------|--------------|
146
189
  | `SeededRNG` | One seed drives everything; `.derive(label)` yields independent, reproducible sub-streams (unifies stdlib + numpy). |
147
190
  | `NameGenerator` | Order-k character Markov names over hand-authored culture namebases; reproducible across processes. |
148
- | `RegionalTerrainGenerator` | Voronoi cells (Lloyd-relaxed) → noise-shaped **organic** land/sea mask + distance-to-coast elevation → Planchon–Darboux depression fill → flux + hydraulic/creep erosion → rivers + inland lakes → latitude/elevation climate with **rain-shadow** → Whittaker biomes. |
191
+ | `RegionalTerrainGenerator` | Voronoi cells (Lloyd-relaxed) → **tectonic-plate** heightmap (organic coasts + mountain ranges at plate collisions; percentile sea level) → Planchon–Darboux depression fill → flux + hydraulic/creep erosion → rivers + inland lakes → latitude/elevation climate with **rain-shadow** → Whittaker biomes. |
149
192
  | `compute_cell_polygons` | Reconstructs convex Voronoi polygons (half-plane clipping) for vector rendering. |
150
- | `RegionalSVGRenderer` | Shaded-relief (hillshade) SVG: biome polygons, coastline, rivers, roads, labelled markers. |
193
+ | `RegionalSVGRenderer` | Shaded-relief (hillshade) SVG: biome polygons, coastline, rivers, roads, labelled markers. Takes a `theme=`. |
194
+ | `Theme` / `THEMES` | A render palette + biome vocabulary; re-skins the same terrain (parchment / neon / dune / blueprint, or your own). The "Dominant Medium" layer. |
195
+ | `AtlasRenderer` / `ArtPack` | Hand-drawn / themed PNG: stamps symbols from an external *art pack* (mountains, forests, hills, settlements, sea decorations) onto the terrain. mapwright ships no art — a pack is a skin. Needs `pip install "mapwright[atlas]"`. |
151
196
  | `RegionalRoadGenerator` | Connects settlement sites with trade routes — an MST whose edges are A*-routed over the terrain (avoids sea, climbs/crosses rivers at a cost). |
152
197
  | `RegionGenerator` | Partitions land into named factions/territories: spread capitals seed a flood fill over the land graph (sea divides them); each `Region` is Markov-named. |
153
198
  | `DungeonGenerator` | BSP-partitioned rooms + minimum-spanning-tree corridors → rooms, corridor cells, and a walkable grid (with `Dungeon.ascii()`). |
@@ -158,6 +203,77 @@ Settlement presets: `hamlet`, `village`, `town`, `city`, `port`, `citadel`.
158
203
  Everything is neutral: `RegionalTerrainGenerator` returns a `TerrainResult` of `TerrainCell`s
159
204
  (each with a `Biome`), and you decide how a `Biome` maps to your world.
160
205
 
206
+ ## Atlas rendering & art packs
207
+
208
+ `RegionalSVGRenderer` draws a clean shaded-relief map. For a **hand-drawn** (or neon, or
209
+ scrap-metal, or any) look, `AtlasRenderer` stamps little symbol images — mountains, trees,
210
+ hills, towns, sea monsters, a compass — placed exactly where the physics put them.
211
+
212
+ mapwright bundles **no art**. The renderer is the *engine*; the art is a separate **art pack**
213
+ you point it at, so the same world can wear any style without re-generating anything:
214
+
215
+ ```python
216
+ from mapwright import SeededRNG, RegionalTerrainGenerator, ArtPack, AtlasRenderer, Marker
217
+
218
+ terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(80, 56)
219
+ markers = [Marker("Eldmoor", 40, 28, kind="settlement_castle")]
220
+
221
+ pack = ArtPack.from_directory("path/to/my-art-pack") # needs mapwright[atlas]
222
+ png = AtlasRenderer(pack, scale=12, seed=7).render(terrain, markers, land_age=0.3)
223
+ open("atlas.png", "wb").write(png)
224
+ ```
225
+
226
+ An **art pack** is just a directory of transparent PNG symbols plus an optional
227
+ `manifest.json` that maps mapwright's neutral concepts onto art **slots**:
228
+
229
+ ```jsonc
230
+ {
231
+ "name": "my-pack",
232
+ "colors": {"parchment": "#ecdfbf", "water": "#b5cad1",
233
+ "coast": "#463c2c", "label": "#2b2218"},
234
+ "slots": {
235
+ "mountain.young": {"files": ["mountains/sharp/*.png"], "width": 2.0, "anchor": "bottom"},
236
+ "mountain.old": {"files": ["mountains/eroded/*.png"]},
237
+ "hill": {"files": ["hills/*.png"]},
238
+ "tree.pine": {"files": ["trees/pine/*.png"]},
239
+ "tree.deciduous": {"files": ["trees/leafy/*.png"]},
240
+ "city.castle": {"files": ["cities/castle*.png"]},
241
+ "decoration.compass": {"files": ["compass/*.png"], "anchor": "center"}
242
+ }
243
+ }
244
+ ```
245
+
246
+ Slots the renderer asks for: terrain relief — `mountain.young` / `mountain.mid` /
247
+ `mountain.old` (chosen by `land_age`), `hill`, `tree.pine` / `tree.deciduous` /
248
+ `tree.cactus` (by climate), `dune`; settlements — `city.castle` / `city.large` /
249
+ `city.town` / `city.village` (by marker `kind`); decorations — `decoration.creature`
250
+ / `decoration.ship` / `decoration.compass`. A missing fine slot falls back to a coarser
251
+ sibling (`mountain.mid` → any `mountain.*`), so partial packs still render. With **no**
252
+ `manifest.json`, `ArtPack.from_directory()` auto-discovers slots from a conventional
253
+ folder layout. Because packs are pure data, a host like an image-generation service can
254
+ **produce them on demand** in any style — the generation stays the same; the pack is the skin.
255
+
256
+ ### Render themes
257
+
258
+ The vector `RegionalSVGRenderer` takes a **`Theme`** — a palette plus an optional biome
259
+ *vocabulary* — so the same neutral terrain re-skins into wildly different worlds without
260
+ regenerating anything. The neutral `Biome` enum never changes; a theme just decides how
261
+ each biome looks and is named:
262
+
263
+ ```python
264
+ from mapwright import RegionalSVGRenderer, THEMES
265
+
266
+ svg = RegionalSVGRenderer(theme="neon").render(terrain, markers, roads=roads)
267
+ # built-ins: "parchment" (default), "neon" (Tron/digital-grid), "dune" (sand), "blueprint"
268
+ THEMES["neon"].biome_label(Biome.OCEAN) # -> "Void" (the vocabulary layer)
269
+ ```
270
+
271
+ A `Theme` is plain hex-string data (JSON-friendly), so a host — or the same image service
272
+ that makes art packs — can author new ones. This is the "Dominant Medium" idea from
273
+ mapwright's longer-term vision: a sand planet, a digital grid, and an irradiated waste are
274
+ the *same map* wearing different skins. Pair a theme with a matching `ArtPack` for a full
275
+ restyle of both the vector and hand-drawn renders.
276
+
161
277
  ## Determinism
162
278
 
163
279
  Every generator draws from a `SeededRNG`. The same seed (and parameters) reproduces an