settlemaker 3.0.0 → 3.0.1

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 (55) hide show
  1. package/README.md +162 -277
  2. package/dist/output/geojson-builder.d.ts +1 -1
  3. package/dist/output/geojson-builder.js +1 -1
  4. package/dist/settlemaker.browser.js +1 -1
  5. package/docs/README.md +54 -0
  6. package/docs/api.md +175 -0
  7. package/docs/artwork-integration.md +104 -0
  8. package/docs/current-symbol-library.md +62 -42
  9. package/docs/development.md +102 -0
  10. package/docs/examples/gallery/coastal-city.png +0 -0
  11. package/docs/examples/gallery/coastal-city.svg +4253 -0
  12. package/docs/examples/gallery/coastal-village.png +0 -0
  13. package/docs/examples/gallery/coastal-village.svg +1211 -0
  14. package/docs/examples/gallery/copperline-city.png +0 -0
  15. package/docs/examples/gallery/copperline-city.svg +1139 -0
  16. package/docs/examples/gallery/copperline-village.png +0 -0
  17. package/docs/examples/gallery/copperline-village.svg +2382 -0
  18. package/docs/examples/gallery/desert-city.png +0 -0
  19. package/docs/examples/gallery/desert-city.svg +3509 -0
  20. package/docs/examples/gallery/desert-village.png +0 -0
  21. package/docs/examples/gallery/desert-village.svg +455 -0
  22. package/docs/examples/gallery/fixtures.json +449 -0
  23. package/docs/examples/gallery/temperate-city-night.png +0 -0
  24. package/docs/examples/gallery/temperate-city-night.svg +3511 -0
  25. package/docs/examples/gallery/temperate-city.png +0 -0
  26. package/docs/examples/gallery/temperate-city.svg +3511 -0
  27. package/docs/examples/gallery/temperate-village.png +0 -0
  28. package/docs/examples/gallery/temperate-village.svg +3227 -0
  29. package/docs/examples/gallery/tropical-city.png +0 -0
  30. package/docs/examples/gallery/tropical-city.svg +3508 -0
  31. package/docs/examples/gallery/tropical-village.png +0 -0
  32. package/docs/examples/gallery/tropical-village.svg +3761 -0
  33. package/docs/examples/gallery/tundra-city.png +0 -0
  34. package/docs/examples/gallery/tundra-city.svg +3509 -0
  35. package/docs/examples/gallery/tundra-village.png +0 -0
  36. package/docs/examples/gallery/tundra-village.svg +1549 -0
  37. package/docs/fmg-embed-zoom.md +54 -0
  38. package/docs/gallery.md +139 -0
  39. package/docs/geojson.md +188 -0
  40. package/docs/getting-started.md +157 -0
  41. package/docs/landscape-character-review.md +55 -0
  42. package/docs/releases/3.0.1.md +18 -0
  43. package/docs/scene-schema.md +160 -0
  44. package/docs/schema-v3.md +38 -0
  45. package/docs/skins.md +52 -0
  46. package/docs/test-urls.md +39 -0
  47. package/docs/url-api.md +252 -0
  48. package/docs/water-context-v1.md +384 -0
  49. package/package.json +15 -8
  50. package/scripts/check-docs.mjs +106 -0
  51. package/scripts/generate-copperline.mjs +255 -0
  52. package/scripts/generate-examples.mjs +54 -0
  53. package/scripts/render-copperline.mjs +36 -0
  54. package/src/output/geojson-builder.ts +1 -1
  55. package/symbols/copperline/README.md +10 -3
@@ -0,0 +1,255 @@
1
+ /** Original top-down vector artwork. Run: node scripts/generate-copperline.mjs */
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { SKIN_SLOTS as manifest } from '../dist/index.js';
5
+
6
+ const root = new URL('../', import.meta.url);
7
+ const palette = {
8
+ ink: '#293b40', steel: '#879ca0', light: '#cad4ce', dark: '#526c73',
9
+ copper: '#bc8050', brass: '#e1b96e', glass: '#91d2d0', glassLine: '#d9efdf',
10
+ paving: '#c2c8ba', seam: '#a7b2a5', green: '#658778', leaf: '#8ca68a',
11
+ };
12
+ const paint = key => `var(--cl-${key}, ${palette[key]})`;
13
+ const rect = (x, y, w, h, fill = 'steel', radius = 0, sw = 1.6) =>
14
+ `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" fill="${paint(fill)}" stroke="${paint('ink')}" stroke-width="${sw}"/>`;
15
+ const path = (d, fill = 'none', sw = 1.2, stroke = 'ink') =>
16
+ `<path d="${d}" fill="${fill === 'none' ? 'none' : paint(fill)}" stroke="${paint(stroke)}" stroke-width="${sw}" stroke-linejoin="round" stroke-linecap="round"/>`;
17
+ const circle = (x, y, r, fill = 'copper', sw = 1.2) =>
18
+ `<circle cx="${x}" cy="${y}" r="${r}" fill="${paint(fill)}" stroke="${paint('ink')}" stroke-width="${sw}"/>`;
19
+ const fan = (x, y, r = 4) => circle(x, y, r, 'dark') + circle(x, y, r * .7, 'light', .6)
20
+ + path(`M${x-r*.5} ${y-r*.5}l${r} ${r}m0 ${-r}l${-r} ${r}`, 'none', .7) + circle(x, y, .8, 'brass', 0);
21
+ const glass = (x, y, w, h) => rect(x, y, w, h, 'glass', .3, .8)
22
+ + path(`M${x+w/2} ${y}v${h}M${x} ${y+h/2}h${w}`, 'none', .65, 'glassLine');
23
+ const rivets = (x, y, w, h) => [[x,y],[x+w,y],[x,y+h],[x+w,y+h]].map(([a,b]) => circle(a,b,.65,'brass',0)).join('');
24
+ const roof = (x, y, w, h, fill = 'steel') => rect(x,y,w,h,fill,1)
25
+ + rect(x+2.5,y+2.5,w-5,h-5,fill,.3,.65) + rivets(x+1.2,y+1.2,w-2.4,h-2.4);
26
+ const door = (x, y, w = 8) => rect(x,y,w,2.5,'brass',0,.7);
27
+ const pipe = d => path(d,'none',3.2,'ink') + path(d,'none',1.8,'copper');
28
+ const art = {};
29
+ const names = {};
30
+ function add(id, name, body, silhouette) {
31
+ art[id] = { body: `<g stroke-linejoin="round" stroke-linecap="round">${body}</g>`,
32
+ ...(silhouette ? { sil: silhouette } : {}) };
33
+ names[id] = name;
34
+ }
35
+ const boxSil = (x,y,w,h,r=1) => `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${r}" fill="currentColor"/>`;
36
+ const diskSil = (x,y,r) => `<circle cx="${x}" cy="${y}" r="${r}" fill="currentColor"/>`;
37
+
38
+ // Residential ink stays inside the original house frontage (roughly x=12..52).
39
+ add('sm-house', 'Workshop residence', roof(13,17,38,30) + glass(18,22,17,10)
40
+ + fan(43,26) + pipe('M39 36H45V42H35') + door(27,47), boxSil(12,16,40,34));
41
+ add('sm-house-tiled', 'Solar terrace', roof(12,17,40,31,'light') + glass(17,22,24,14)
42
+ + path('M25 22V36M33 22V36','none',.7) + fan(46,25,2.5) + rect(19,40,22,3,'copper',0,.7)
43
+ + door(28,48), boxSil(11,16,42,35));
44
+ add('sm-house-large-tiled', 'Engineering headquarters', roof(8,14,48,36,'light')
45
+ + glass(14,20,20,24) + roof(38,20,12,24,'dark') + fan(44,27,3.5) + fan(44,37,3.5)
46
+ + door(26,50,12), boxSil(7,13,50,40));
47
+ add('sm-longhouse', 'Sawtooth factory', roof(8,22,48,20,'copper')
48
+ + [12,23,34,45].map(x => rect(x,25,7,13,'light',0,.7) + glass(x,25,3,13)).join('')
49
+ + door(27,42,10), boxSil(7,21,50,25));
50
+ add('sm-hut-straw', 'Prefab cabin', roof(14,15,36,35) + glass(19,20,17,12)
51
+ + fan(43,24,3) + pipe('M21 40H41V35') + door(28,50), boxSil(13,14,38,39));
52
+ add('sm-hut-round', 'Pressure dome home', circle(32,32,20,'copper',1.8)
53
+ + circle(32,32,16,'light',.8) + path('M17 26Q32 18 47 26M17 38Q32 46 47 38','none',.8)
54
+ + glass(24,25,16,14) + door(28,51), diskSil(32,32,21)+boxSil(27,49,10,5));
55
+ add('sm-hut-mud', 'Service pod', roof(15,14,34,37,'copper') + circle(32,29,10,'light')
56
+ + fan(32,29,7) + rect(22,43,20,4,'dark',0,.8) + door(28,51), boxSil(14,13,36,41));
57
+ add('sm-inn', 'Transit hotel', path('M10 12H54V52H40V30H24V52H10Z','light',1.8)
58
+ + glass(15,17,34,8) + glass(15,32,5,15) + glass(44,32,5,15)
59
+ + rect(27,34,10,13,'paving',0,.6) + circle(32,40,3,'green',.7)
60
+ + door(26,26,12), '<path d="M9 11H55V53H39V31H25V53H9Z" fill="currentColor"/>');
61
+ add('sm-chapel', 'Turbine exchange', roof(17,10,30,43,'light') + pipe('M21 18H43V46H21')
62
+ + fan(32,24,8) + fan(32,42,6) + door(27,53,10), boxSil(16,9,32,47));
63
+ add('sm-cathedral', 'Central generating station', roof(8,17,48,32,'copper')
64
+ + glass(22,22,20,22) + [16,48].map(x => fan(x,26,4.5)+fan(x,39,4.5)).join('')
65
+ + pipe('M25 13H46V20M19 47V53H38') + circle(17,13,5,'light') + circle(17,13,2.7,'dark')
66
+ + circle(47,53,5,'light') + circle(47,53,2.7,'dark'), boxSil(7,16,50,34)+diskSil(17,13,6)+diskSil(47,53,6));
67
+ add('sm-temple', 'Research observatory', roof(10,17,44,32,'light') + circle(32,32,17,'copper',1.6)
68
+ + circle(32,32,13,'glass',.8) + path('M22 22L42 42M24 20L44 40','none',2,'light')
69
+ + circle(45,20,4,'brass') + door(27,49,10), boxSil(9,16,46,36)+diskSil(32,32,18));
70
+ add('sm-stone-circle', 'District heat manifold', circle(32,32,23,'paving',.7)
71
+ + circle(32,32,16,'copper',2) + circle(32,32,11,'dark')
72
+ + [0,60,120,180,240,300].map(a => `<g transform="rotate(${a} 32 32)">${pipe('M32 16V10')+rect(28,6,8,7,'light',1,.8)}</g>`).join('')
73
+ + fan(32,32,8), diskSil(32,32,24));
74
+ add('sm-well', 'Public water pump', rect(17,17,30,30,'paving',3,.8) + circle(32,32,11,'copper',1.6)
75
+ + circle(32,32,7,'glass') + pipe('M32 32H47V41') + circle(32,32,2,'brass'), boxSil(16,16,32,32,3));
76
+ add('sm-boathouse--coastal', 'Dry dock workshop', roof(8,22,48,20,'dark') + glass(12,25,30,5)
77
+ + path('M14 39V33H50V39','none',2,'brass') + [17,29,41].map(x=>rect(x,35,8,5,'copper',0,.6)).join(''), boxSil(7,21,50,22));
78
+
79
+ // Continuous barriers retain the kit's full 64-unit pitch.
80
+ add('sm-kit-wall', 'Steel security barrier', rect(0,25,64,14,'light',0,1)
81
+ + path('M0 29H64M0 35H64','none',1,'dark') + [8,24,40,56].map(x=>rect(x,24,3,16,'steel',0,.7)).join(''), boxSil(0,24,64,16,0));
82
+ add('sm-kit-corner', 'Barrier elbow', path('M0 25H39V64H25V39H0Z','light',1)
83
+ + path('M0 29H35V64M0 35H29V64','none',1,'dark'), '<path d="M0 24H40V64H24V40H0Z" fill="currentColor"/>');
84
+ add('sm-kit-gate', 'Checkpoint', rect(0,27,64,10,'light',0,1) + roof(8,17,14,30)
85
+ + roof(42,17,14,30) + glass(11,21,8,10) + glass(45,21,8,10)
86
+ + rect(22,30,20,4,'brass',0,.8) + path('M25 30L29 34M33 30L37 34','none',1.8),
87
+ boxSil(0,26,64,12,0)+boxSil(7,16,16,32)+boxSil(41,16,16,32));
88
+ add('sm-kit-tower-drum', 'Cooling tower', circle(32,32,21,'light',1.8) + circle(32,32,16,'copper')
89
+ + fan(32,32,12) + [0,90,180,270].map(a=>`<g transform="rotate(${a} 32 32)">${rect(29,11,6,5,'steel',0,.7)}</g>`).join(''), diskSil(32,32,22));
90
+ add('sm-kit-tower-square', 'Communications mast', roof(14,14,36,36,'light')
91
+ + path('M21 21L43 43M43 21L21 43','none',3,'steel') + circle(32,32,9,'copper')
92
+ + circle(32,32,5,'glass') + path('M32 18V46M18 32H46','none',1.1), boxSil(13,13,38,38));
93
+ add('sm-kit-keep', 'Central control complex', roof(9,14,46,36,'dark') + glass(17,20,30,22)
94
+ + roof(11,16,9,11,'light') + roof(44,16,9,11,'light') + fan(15.5,21.5,2.5) + fan(48.5,21.5,2.5)
95
+ + door(25,50,14), boxSil(8,13,48,40));
96
+
97
+ add('sm-tree-deciduous', 'Street tree', circle(32,32,20,'green',.9)
98
+ + circle(27,27,12,'leaf',0) + path('M23 39Q34 44 43 33','none',1,'dark'));
99
+ add('sm-tree-deciduous-small', 'Planter tree', rect(16,16,32,32,'paving',4,.65)
100
+ + circle(32,32,14,'green',.8) + circle(28,28,9,'leaf',0));
101
+ add('sm-tree-conifer', 'Formal evergreen', path('M32 10L43 18L49 32L42 45L32 53L21 45L15 32L21 19Z','green',.8)
102
+ + path('M32 16V47M24 25L32 31L40 25M23 36L32 41L41 36','none',1,'leaf'));
103
+
104
+ const greenShapes = {
105
+ round: 'M8 32C8 17 20 9 33 10C49 10 56 21 55 34C54 49 43 55 29 54C15 53 8 45 8 32Z',
106
+ lens: 'M5 32Q31 12 59 32Q32 52 5 32Z',
107
+ 'lens-long': 'M4 32Q32 20 60 32Q32 44 4 32Z',
108
+ triangle: 'M29 9Q32 5 35 10L56 48Q59 54 51 54H13Q6 54 9 48Z',
109
+ square: 'M13 13H51Q56 13 56 19V46Q56 51 50 51H14Q8 51 8 45V19Q8 13 13 13Z',
110
+ d: 'M8 49V34C8 6 56 6 56 34V49Z',
111
+ };
112
+ for (const [shape,d] of Object.entries(greenShapes)) for (const variant of ['a','b']) {
113
+ add(`sm-green-${shape}-${variant}`, `Paved ${shape} ${variant}`,
114
+ path(d,'paving',2.2,'seam') + (variant === 'a'
115
+ ? circle(25,31,.65,'seam',0)+circle(37,35,.65,'seam',0)
116
+ : circle(28,34,.65,'seam',0)+circle(40,31,.65,'seam',0)));
117
+ }
118
+ // Periodic tiles: all details are inset; full-span lines meet their opposite edge.
119
+ const tile = body => rect(0,0,64,64,'paving',0,0)+body;
120
+ add('sm-field-plough', 'Solar array', tile([8,36].map(y=>[6,36].map(x =>
121
+ rect(x,y,22,14,'dark',0,.45) + glass(x+1,y+1,20,12)).join('')).join('')));
122
+ add('sm-field-stubble', 'Container yard', tile([8,36].map((y,i)=>[6,36].map(x =>
123
+ rect(x,y,22,14,i?'steel':'copper',0,.5) + path(`M${x+5} ${y+2}v10m6 -10v10m6 -10v10`,'none',.5,'light')).join('')).join('')));
124
+ add('sm-field-fallow', 'Concrete service yard', tile(path('M0 16H64M0 48H64M16 0V16M48 16V48M16 48V64','none',.5,'seam')));
125
+ add('sm-field-pasture', 'Managed lawn', rect(0,0,64,64,'leaf',0,0)
126
+ + [8,24,40,56].map(y=>path(`M0 ${y}H64`,'none',.45,'green')).join(''));
127
+ add('sm-field-orchard', 'Tank farm', tile([16,48].map(y=>[16,48].map(x=>
128
+ circle(x,y,8,'light',.65)+circle(x,y,5.5,'steel',.45)+path(`M${x-5} ${y}h10`,'none',.5,'light')).join('')).join('')));
129
+ add('sm-field-vine', 'Greenhouse beds', tile([7,37].map(y=>glass(5,y,54,20)).join('')));
130
+ add('sm-field-paddy--tropical', 'Water treatment beds', tile([7,37].map(y=>
131
+ rect(5,y,54,20,'glass',1,.6)+path(`M8 ${y+7}H56M8 ${y+13}H56`,'none',.6,'glassLine')).join('')));
132
+ add('sm-field-irrigated--desert', 'Cooled growing beds', tile([8,40].map(y=>rect(4,y,56,16,'leaf',0,.5)).join('')
133
+ + path('M0 2H64M0 34H64','none',2,'glass')));
134
+ add('sm-edge-hedge', 'Clipped hedge', rect(0,28,64,8,'green',0,.6)
135
+ + path('M0 31H64','none',1.2,'leaf'));
136
+ add('sm-edge-wall', 'Concrete kerb', rect(0,29,64,6,'light',0,.7)
137
+ + path('M16 29V35M48 29V35','none',.6,'seam'));
138
+ add('sm-edge-fence', 'Wire security fence', path('M0 29H64M0 35H64','none',.7,'dark')
139
+ + [0,16,32,48].map(x=>path(`M${x} 29l8 6l8 -6M${x} 35l8 -6l8 6`,'none',.45,'steel')).join('')
140
+ + [8,40].map(x=>rect(x,27,1.5,10,'dark',0,.4)).join(''));
141
+ add('sm-edge-ditch', 'Storm drain', rect(0,28,64,8,'dark',0,.5)
142
+ + [4,12,20,28,36,44,52,60].map(x=>path(`M${x} 29v6`,'none',.7,'steel')).join(''));
143
+
144
+ // Adapt the original drawings to current runtime slots, retaining each slot's
145
+ // own art box. The generator now places native city, flora, field and henge IDs.
146
+ const cityFamilies = {
147
+ 'row-house-a': 'sm-house', 'row-house-b': 'sm-house-tiled',
148
+ terrace: 'sm-longhouse', tenement: 'sm-house-large-tiled',
149
+ corner: 'sm-house-tiled', courtyard: 'sm-house-large-tiled',
150
+ 'courtyard-u': 'sm-house-large-tiled', 'shop-house': 'sm-house',
151
+ inn: 'sm-inn', warehouse: 'sm-longhouse', workshop: 'sm-house-large-tiled',
152
+ guildhall: 'sm-inn', bathhouse: 'sm-temple', 'market-hall': 'sm-longhouse',
153
+ cathedral: 'sm-cathedral', chapel: 'sm-chapel', church: 'sm-chapel',
154
+ cloister: 'sm-temple', 'temple-court': 'sm-temple', 'temple-hall': 'sm-temple',
155
+ 'castle-wall': 'sm-kit-wall', 'castle-gatehouse': 'sm-kit-gate',
156
+ 'castle-tower-round': 'sm-kit-tower-drum', 'castle-tower-square': 'sm-kit-tower-square',
157
+ 'castle-keep': 'sm-kit-keep', 'castle-hall': 'sm-longhouse',
158
+ 'castle-barracks': 'sm-longhouse', 'castle-chapel': 'sm-chapel',
159
+ 'palace-hall': 'sm-house-large-tiled', 'palace-wing': 'sm-longhouse',
160
+ 'palace-gallery': 'sm-longhouse', 'palace-pavilion': 'sm-hut-round',
161
+ 'palace-gatehouse': 'sm-kit-gate', 'palace-chapel': 'sm-chapel',
162
+ };
163
+ for (const [id,meta] of Object.entries(manifest)) {
164
+ if (art[id]) continue;
165
+ const base = id.split('--')[0];
166
+ let target = art[base] ? base : cityFamilies[meta.family]
167
+ ?? (base === 'sm-hut' ? 'sm-hut-straw' : base === 'sm-kit-tower' ? 'sm-kit-tower-square' : undefined);
168
+ if (!target && meta.category === 'henge') target = 'sm-stone-circle';
169
+ if (!target && meta.category === 'field') {
170
+ target = /grove|orchard/.test(meta.family) ? 'sm-field-orchard'
171
+ : /paddy/.test(meta.family) ? 'sm-field-paddy--tropical'
172
+ : /garden|alfalfa/.test(meta.family) ? 'sm-field-vine'
173
+ : meta.natural || /pasture|meadow|grazing|lichen/.test(meta.family) ? 'sm-field-pasture'
174
+ : /rest|fallow/.test(meta.family) ? 'sm-field-fallow'
175
+ : /stubble/.test(meta.family) ? 'sm-field-stubble' : 'sm-field-plough';
176
+ }
177
+ if (!target && (meta.category === 'flora' || meta.zBand === 'canopy')) {
178
+ target = /conifer|snag|spruce|pine/.test(id) ? 'sm-tree-conifer'
179
+ : /scrub|grass|fern|moss|lichen|flowers|sedge|shrub/.test(id) ? 'sm-tree-deciduous-small' : 'sm-tree-deciduous';
180
+ }
181
+ if (!target || !art[target]) throw new Error(`Missing Copperline artwork: ${id}`);
182
+ const [x,y,w,h] = meta.viewBox;
183
+ const wrap = markup => w === 64 && h === 64 && x === 0 && y === 0 ? markup
184
+ : `<g transform="translate(${x} ${y}) scale(${w/64} ${h/64})">${markup}</g>`;
185
+ art[id] = { body: wrap(art[target].body),
186
+ ...(meta.zBand === 'structure' && art[target].sil ? {sil: wrap(art[target].sil)} : {}) };
187
+ }
188
+ const tokens = Object.fromEntries(Object.entries(palette).map(([k,v])=>[`--cl-${k}`,v]));
189
+ Object.assign(tokens, {
190
+ '--sm-ink': palette.ink, '--sm-stone': palette.light, '--sm-timber': palette.steel,
191
+ '--sm-void': palette.dark, '--sm-lead': palette.steel, '--sm-common': palette.paving,
192
+ '--sm-common-band': palette.seam, '--sm-soil': palette.paving, '--sm-furrow': palette.seam,
193
+ '--sm-canopy-a': palette.green, '--sm-canopy-b': palette.leaf, '--sm-yard': palette.paving,
194
+ });
195
+ const skin = {
196
+ version: 1, id: 'copperline', name: 'Copperline', tokens,
197
+ village: { ground: '#b5bfaf', water: '#709ca2', waterEdge: '#526f78', shadowColor: '#263c42', shadowOpacity: .2 },
198
+ city: { paper: '#b5bfaf', water: '#709ca2', waterEdge: '#526f78', fieldFill: '#c2c8ba', fieldFurrow: '#a7b2a5',
199
+ greenFill: '#8ca68a', treeFill: '#658778', roadCore: '#d4d7ca', roadCasing: '#87938d',
200
+ buildingFill: '#879ca0', buildingStroke: '#293b40', landmarkFill: '#bc8050', shadowColor: '#263c42', shadowOpacity: .2 },
201
+ glyphs: art,
202
+ biomes: {
203
+ industrial: { base: 'temperate', aliases: ['copperline', 'industrial district'] },
204
+ steampunk: { base: 'temperate', aliases: ['brassworks', 'steam city'],
205
+ tokens: { '--cl-steel': '#9c8e78', '--cl-light': '#ded0aa', '--cl-dark': '#615951', '--cl-copper': '#b97543',
206
+ '--cl-brass': '#f0ca77', '--cl-glass': '#9eb8a5', '--cl-paving': '#d1c2a3', '--cl-seam': '#b5a58b',
207
+ '--sm-common': '#d1c2a3', '--sm-common-band': '#b5a58b', '--sm-furrow': '#b5a58b' },
208
+ village: { ground: '#baaf91', water: '#819e99', waterEdge: '#607d7a' },
209
+ city: { paper: '#baaf91', water: '#819e99', waterEdge: '#607d7a', roadCore: '#dfd2b3', roadCasing: '#9d917b',
210
+ buildingFill: '#9c8e78', landmarkFill: '#b97543', fieldFill: '#d1c2a3', fieldFurrow: '#b5a58b' } },
211
+ modern: { base: 'temperate', aliases: ['technology park', 'modern city'],
212
+ tokens: { '--cl-steel': '#9eafb8', '--cl-light': '#e1e9e5', '--cl-dark': '#456879', '--cl-copper': '#658995',
213
+ '--cl-brass': '#efc971', '--cl-glass': '#83c8e3', '--cl-glassLine': '#e5f5fa', '--cl-paving': '#d1d9d3',
214
+ '--cl-seam': '#b6c2bd', '--sm-common': '#d1d9d3', '--sm-common-band': '#b6c2bd', '--sm-furrow': '#b6c2bd' },
215
+ village: { ground: '#b9caba', water: '#80afc5', waterEdge: '#628da3' },
216
+ city: { paper: '#b9caba', water: '#80afc5', waterEdge: '#628da3', roadCore: '#e3e7de', roadCasing: '#99aaa7',
217
+ buildingFill: '#9eafb8', landmarkFill: '#658995', fieldFill: '#d1d9d3', fieldFurrow: '#b6c2bd' } },
218
+ },
219
+ };
220
+ // Token names must be lowercase under the portable skin contract.
221
+ const normalize = value => JSON.parse(JSON.stringify(value).replaceAll('--cl-glassLine', '--cl-glass-line'));
222
+ const definition = normalize(skin);
223
+ mkdirSync(new URL('symbols/copperline/', root), { recursive: true });
224
+ writeFileSync(new URL('docs/examples/copperline.skin.json', root), JSON.stringify(definition,null,2)+'\n');
225
+ const symbols = Object.entries(definition.glyphs).map(([id,g]) =>
226
+ `<symbol id="${id}" viewBox="${manifest[id].viewBox.join(' ')}">${g.body}</symbol>`
227
+ + (g.sil ? `\n<symbol id="${id}-sil" viewBox="${manifest[id].viewBox.join(' ')}">${g.sil}</symbol>` : '')).join('\n');
228
+ writeFileSync(new URL('symbols/copperline/symbols.svg', root), `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">\n${symbols}\n</svg>\n`);
229
+
230
+ // A self-contained contact sheet with resolved paint for SVG viewers/rasterizers.
231
+ const resolvePaint = (markup, selected) => markup.replace(/var\((--[a-z-]+),\s*[^)]+\)/g, (_,key)=>selected[key]);
232
+ const displayIds = Object.keys(names);
233
+ const cols = 6, rows = Math.ceil(displayIds.length/cols), height = 282+rows*132+68;
234
+ let sheet = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="${height}" viewBox="0 0 1200 ${height}" role="img" aria-labelledby="title desc">
235
+ <title id="title">Copperline — industrial settlement artwork</title><desc id="desc">Top-down SVG buildings, infrastructure and landscape tiles. Three material palettes: industrial, steampunk and modern.</desc>
236
+ <rect width="1200" height="${height}" fill="#f0eee5"/>
237
+ <g font-family="sans-serif" fill="#293b40"><text x="40" y="49" font-size="13" letter-spacing="3">SETTLEMAKER / VECTOR BIOME 01</text>
238
+ <text x="40" y="98" font-size="46" font-weight="700">COPPERLINE</text>
239
+ <text x="41" y="127" font-size="15">Rooftop machinery · copper pipework · glass roofs · engineered grounds</text>`;
240
+ Object.entries(definition.biomes).forEach(([key,preset],i)=>{
241
+ const selected = {...definition.tokens,...preset.tokens};
242
+ sheet += `<rect x="${40+i*382}" y="152" width="366" height="97" rx="6" fill="${preset.village?.ground ?? definition.village.ground}"/>`
243
+ + `<text x="${56+i*382}" y="178" font-size="12" font-weight="700" letter-spacing="2">${key.toUpperCase()}</text>`
244
+ + ['sm-house','sm-kit-tower-drum','sm-temple'].map((id,j)=>`<g transform="translate(${167+i*382+j*72} 174)">${resolvePaint(definition.glyphs[id].body,selected)}</g>`).join('');
245
+ });
246
+ displayIds.forEach((id,i)=>{
247
+ const x=40+(i%cols)*188, y=282+Math.floor(i/cols)*132;
248
+ sheet += `<rect x="${x}" y="${y}" width="180" height="122" rx="5" fill="#e1e3d7"/>`
249
+ + `<g transform="translate(${x+58} ${y+7})">${definition.glyphs[id].sil ? `<g transform="translate(2 3)" color="#293b40" opacity=".15">${definition.glyphs[id].sil}</g>` : ''}${resolvePaint(definition.glyphs[id].body,definition.tokens)}</g>`
250
+ + `<text x="${x+90}" y="${y+88}" text-anchor="middle" font-size="11" font-weight="700">${names[id]}</text>`
251
+ + `<text x="${x+90}" y="${y+106}" text-anchor="middle" font-size="8.5" fill="#627570">${id}</text>`;
252
+ });
253
+ sheet += `<text x="40" y="${height-25}" font-size="12">${Object.keys(art).length} covered slots / ${displayIds.length} distinct drawings / native slot dimensions / structure shadows included</text></g></svg>\n`;
254
+ writeFileSync(new URL('symbols/copperline/preview.svg',root),sheet);
255
+ console.log(`Copperline: ${Object.keys(art).length} slots. Preview: ${fileURLToPath(new URL('symbols/copperline/preview.svg',root))}`);
@@ -0,0 +1,54 @@
1
+ /** Generate the public documentation gallery from the compiled package API. */
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import sharp from 'sharp';
6
+ import { createSkin, encodeBurgParam, generateSettlement, PALETTES, SETTLEMAKER_VERSION } from '../dist/index.js';
7
+
8
+ const root = new URL('../', import.meta.url);
9
+ const out = new URL('docs/examples/gallery/', root);
10
+ mkdirSync(out, { recursive: true });
11
+ const flags = { port: false, citadel: false, walls: false, plaza: true, temple: true, shanty: false, capital: false };
12
+ const roads = [
13
+ { bearing_deg: 18, kind: 'main', route_id: 'north-road' },
14
+ { bearing_deg: 142, kind: 'town', route_id: 'south-road' },
15
+ { bearing_deg: 267, kind: 'local', route_id: 'west-road' },
16
+ ];
17
+ const fixtures = [
18
+ { id: 'temperate-village', title: 'Ashford — temperate village', description: 'Roads, individual homes, a green and cultivated fields.', burg: { name: 'Ashford', population: 300, biome: 'temperate' } },
19
+ { id: 'temperate-city', title: 'Thornwall — walled city', description: 'Native city buildings, denser central wards, greener outskirts and defensive walls.', burg: { name: 'Thornwall', population: 10000, biome: 'temperate', walls: true, citadel: true, capital: true } },
20
+ { id: 'desert-village', title: 'Qasra — desert village', description: 'Desert buildings, sparse vegetation and irrigated field textures.', burg: { name: 'Qasra', population: 300, biome: 'desert' } },
21
+ { id: 'desert-city', title: 'Qasr — desert city', description: 'Urban desert architecture, religious buildings and courtyards.', burg: { name: 'Qasr', population: 10000, biome: 'desert', walls: true, citadel: true } },
22
+ { id: 'tundra-village', title: 'Snowmere — tundra village', description: 'Snowy dwellings and low tundra vegetation with natural ground instead of arable fields.', burg: { name: 'Snowmere', population: 300, biome: 'tundra' } },
23
+ { id: 'tundra-city', title: 'Frosthold — tundra city', description: 'Northern roofs, fortifications and biome-specific ground.', burg: { name: 'Frosthold', population: 10000, biome: 'tundra', walls: true, citadel: true } },
24
+ { id: 'tropical-village', title: 'Reedbank — tropical river village', description: 'A supplied river enables tropical paddy selection; fields follow real parcels.', burg: { name: 'Reedbank', population: 300, biome: 'tropical', rivers: [{ centreline: [{ x: -500, y: 90 }, { x: 500, y: 90 }], widthM: 6 }] } },
25
+ { id: 'tropical-city', title: 'Palmreach — tropical city', description: 'Tropical architecture and vegetation in the city engine.', burg: { name: 'Palmreach', population: 10000, biome: 'tropical', walls: true, citadel: true } },
26
+ { id: 'coastal-village', title: 'Dunewick — coastal village', description: 'A generated coast from oceanBearing, with coastal buildings and vegetation.', burg: { name: 'Dunewick', population: 300, biome: 'coastal', oceanBearing: 90, roadBearings: [{ bearing_deg: 210, kind: 'town', route_id: 'coast-road' }, { bearing_deg: 315, kind: 'local', route_id: 'dune-path' }] } },
27
+ { id: 'coastal-city', title: 'Saltmouth — port city', description: 'Port infrastructure beside a generated coastline. This is a bearing-based coast, not a measured survey.', burg: { name: 'Saltmouth', population: 12000, biome: 'coastal', port: true, walls: true, harbourSize: 'large', oceanBearing: 90, roadBearings: [200, 300] } },
28
+ { id: 'copperline-village', title: 'Copperline — industrial village', description: 'The same village planner with a complete replacement skin and industrial artwork.', skin: 'copperline', burg: { name: 'Copperline', population: 250, biome: 'industrial' } },
29
+ { id: 'copperline-city', title: 'Brassworks — steampunk city', description: 'Copperline machinery and brass materials on the existing city layout and semantic feature types.', skin: 'copperline', burg: { name: 'Brassworks', population: 2000, biome: 'steampunk' } },
30
+ ].map(f => ({ ...f, seed: 2, burg: { ...flags, roadBearings: roads, ...f.burg } }));
31
+ const copperline = createSkin(JSON.parse(readFileSync(new URL('docs/examples/copperline.skin.json', root), 'utf8')));
32
+ const manifest = [];
33
+ for (const fixture of fixtures) {
34
+ const result = generateSettlement(fixture.burg, { seed: fixture.seed, ...(fixture.skin ? { skin: copperline } : {}) });
35
+ assert.equal(result.kind, fixture.burg.population <= 1000 ? 'village' : 'settlement');
36
+ assert.ok(result.geojson.features.length > 0);
37
+ assert.doesNotMatch(result.svg, /NaN|Infinity/);
38
+ writeFileSync(new URL(`${fixture.id}.svg`, out), result.svg);
39
+ // librsvg does not support every CSS custom-property form; use the renderer-provided fallbacks.
40
+ const rasterSvg = result.svg.replace(/var\(--[^,]+,\s*([^\)]+)\)/g, '$1');
41
+ await sharp(Buffer.from(rasterSvg)).resize({ width: 1200 }).png({ palette: true, quality: 90, effort: 10 }).toFile(fileURLToPath(new URL(`${fixture.id}.png`, out)));
42
+ const previewUrl = fixture.skin ? undefined : `https://settlemaker.com/fmg?i=${await encodeBurgParam(fixture.burg, fixture.seed)}`;
43
+ manifest.push({ ...fixture, engine: result.kind, ...(previewUrl ? { previewUrl } : {}) });
44
+ console.log(`${fixture.id}: ${result.kind}, ${result.geojson.features.length} features`);
45
+ }
46
+ // A palette example shares the exact city input and seed with the default image.
47
+ const city = fixtures.find(f => f.id === 'temperate-city');
48
+ const night = generateSettlement(city.burg, { seed: city.seed, svg: { palette: PALETTES.night } });
49
+ writeFileSync(new URL('temperate-city-night.svg', out), night.svg);
50
+ await sharp(Buffer.from(night.svg.replace(/var\(--[^,]+,\s*([^\)]+)\)/g, '$1'))).resize({ width: 1200 }).png({ palette: true, quality: 90, effort: 10 }).toFile(fileURLToPath(new URL('temperate-city-night.png', out)));
51
+ writeFileSync(new URL('fixtures.json', out), JSON.stringify({ generatorVersion: SETTLEMAKER_VERSION, fixtures: manifest }, null, 2) + '\n');
52
+ const intro = `# Gallery\n\nThese maps were generated with SettleMaker ${SETTLEMAKER_VERSION} through the public\n\`generateSettlement\` API. PNG previews and full SVGs come from the same output.\nThe gallery covers all five built-in artwork biomes in both engines, plus Copperline.\n\n[Exact inputs and seeds](examples/gallery/fixtures.json) · [Regenerate the gallery](development.md#documentation-checks)\n\nHosted preview links carry the saved inputs; the website's deployed generator may\ndiffer from this package version. Skins are loaded by the library API, so the\nCopperline examples link to their SVGs rather than an unsupported skin URL.\n\n`;
53
+ const cards = manifest.map(f => `## ${f.title}\n\n${f.description}\n\nPopulation **${f.burg.population}** · seed **${f.seed}** · engine **${f.engine}**\n\n[![${f.title}](examples/gallery/${f.id}.png)](examples/gallery/${f.id}.svg)\n\n[Full SVG](examples/gallery/${f.id}.svg)${f.previewUrl ? ` · [Hosted preview](${f.previewUrl})` : ' · [Skin JSON](examples/copperline.skin.json)'}\n`).join('\n');
54
+ writeFileSync(new URL('docs/gallery.md', root), intro + cards + '\n## One layout, another palette\n\nThornwall with the same inputs and seed, using `svg.palette: PALETTES.night`.\nCity palettes alter presentation; they do not replace all native material tokens.\nUse a skin for complete artwork and material control.\n\n[![Thornwall with the night palette](examples/gallery/temperate-city-night.png)](examples/gallery/temperate-city-night.svg)\n');
@@ -0,0 +1,36 @@
1
+ /** Run after npm run build: node scripts/render-copperline.mjs */
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import assert from 'node:assert/strict';
4
+ import { createSkin, generateSettlement, SKIN_SLOTS, skinBiomeFor } from '../dist/index.js';
5
+
6
+ const root = new URL('../', import.meta.url);
7
+ const definition = JSON.parse(readFileSync(new URL('docs/examples/copperline.skin.json', root), 'utf8'));
8
+ const skin = createSkin(definition);
9
+ assert.deepEqual(Object.keys(definition.glyphs).sort(), Object.keys(SKIN_SLOTS).sort());
10
+ for (const [id,slot] of Object.entries(SKIN_SLOTS)) {
11
+ assert.equal(Boolean(definition.glyphs[id].sil), slot.zBand === 'structure', `Shadow contract: ${id}`);
12
+ }
13
+ const burg = {
14
+ name: 'Copperline', population: 400, biome: 'industrial', port: false, citadel: false,
15
+ walls: false, plaza: true, temple: true, shanty: false, capital: false,
16
+ roadBearings: [0, 120, 240],
17
+ };
18
+ for (const biome of Object.keys(definition.biomes)) {
19
+ assert.equal(skinBiomeFor(skin, biome).base, 'temperate');
20
+ for (const population of [400, 2500]) {
21
+ const result = generateSettlement({ ...burg, biome, population }, { seed: 42, skin });
22
+ assert.equal(result.kind, population === 400 ? 'village' : 'settlement');
23
+ const label = population === 400 ? 'village' : 'city';
24
+ const ids = [...result.svg.matchAll(/<(?:g|symbol) id="(?:glyph-)?(sm-[^"]+)"/g)]
25
+ .map(m=>m[1]).filter(id => !id.endsWith('-sil'));
26
+ assert.ok(ids.length > 0, 'Map must contain artwork');
27
+ for (const id of ids) {
28
+ assert.ok(definition.glyphs[id], `Missing replacement: ${id}`);
29
+ }
30
+ assert.ok(result.svg.includes('var(--cl-'), 'Map must use Copperline materials');
31
+ if (biome === 'industrial') {
32
+ writeFileSync(new URL(`docs/examples/copperline-${label}.svg`, root), result.svg);
33
+ }
34
+ console.log(`${biome}: ${label}, ${ids.length} artwork definitions; valid`);
35
+ }
36
+ }
@@ -26,7 +26,7 @@ export const GEOJSON_SCHEMA_VERSION = 4;
26
26
  * Source-of-truth library version. Kept in sync with package.json manually —
27
27
  * cheaper than a JSON import assertion and lets tests pin a deterministic value.
28
28
  */
29
- export const SETTLEMAKER_VERSION = '3.0.0';
29
+ export const SETTLEMAKER_VERSION = '3.0.1';
30
30
 
31
31
  export interface GenerateGeoJsonOptions {
32
32
  /** ISO-8601 timestamp to stamp on the output. Defaults to `new Date().toISOString()`. */
@@ -9,8 +9,9 @@ replace the bundled medieval architecture. All 693 runtime skin slots are covere
9
9
 
10
10
  - [Portable skin JSON](../../docs/examples/copperline.skin.json): load with `createSkin`.
11
11
  - [SVG sprite](symbols.svg): all engine slot IDs and structure silhouette twins.
12
- - [Village example](../../docs/examples/copperline-village.svg).
13
- - [City example](../../docs/examples/copperline-city.svg).
12
+ - [Current village example](../../docs/examples/gallery/copperline-village.svg).
13
+ - [Current city example](../../docs/examples/gallery/copperline-city.svg).
14
+ - [Complete gallery and inputs](../../docs/gallery.md).
14
15
 
15
16
  Three named biomes share the same artwork and temperate terrain behaviour:
16
17
 
@@ -24,7 +25,9 @@ Three named biomes share the same artwork and temperate terrain behaviour:
24
25
  import { createSkin, generateSettlement } from 'settlemaker';
25
26
  import { readFile } from 'node:fs/promises';
26
27
 
27
- const skin = createSkin(JSON.parse(await readFile('docs/examples/copperline.skin.json', 'utf8')));
28
+ const definitionUrl = import.meta.resolve('settlemaker/docs/examples/copperline.skin.json');
29
+ const skin = createSkin(JSON.parse(await readFile(new URL(definitionUrl), 'utf8')));
30
+ // burg is a complete AzgaarBurgInput; see the getting-started guide.
28
31
  const result = generateSettlement({ ...burg, biome: 'steampunk' }, { seed: 42, skin });
29
32
  ```
30
33
 
@@ -60,3 +63,7 @@ regenerates the JSON, sprite, and contact sheet; the last
60
63
  validates slot/shadow coverage and renders all three presets through both engines,
61
64
  saving the industrial village and city examples. Original artwork in this folder
62
65
  and the Copperline JSON is distributed under the repository's [GPL-3.0-only licence](../../LICENSE).
66
+
67
+ The public documentation gallery is regenerated separately with `npm run docs:images`.
68
+ Runtime handles must be created in the module instance doing the rendering; retain
69
+ the JSON when saving a skin or transferring it to a worker.