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.
- package/README.md +162 -277
- package/dist/output/geojson-builder.d.ts +1 -1
- package/dist/output/geojson-builder.js +1 -1
- package/dist/settlemaker.browser.js +1 -1
- package/docs/README.md +54 -0
- package/docs/api.md +175 -0
- package/docs/artwork-integration.md +104 -0
- package/docs/current-symbol-library.md +62 -42
- package/docs/development.md +102 -0
- package/docs/examples/gallery/coastal-city.png +0 -0
- package/docs/examples/gallery/coastal-city.svg +4253 -0
- package/docs/examples/gallery/coastal-village.png +0 -0
- package/docs/examples/gallery/coastal-village.svg +1211 -0
- package/docs/examples/gallery/copperline-city.png +0 -0
- package/docs/examples/gallery/copperline-city.svg +1139 -0
- package/docs/examples/gallery/copperline-village.png +0 -0
- package/docs/examples/gallery/copperline-village.svg +2382 -0
- package/docs/examples/gallery/desert-city.png +0 -0
- package/docs/examples/gallery/desert-city.svg +3509 -0
- package/docs/examples/gallery/desert-village.png +0 -0
- package/docs/examples/gallery/desert-village.svg +455 -0
- package/docs/examples/gallery/fixtures.json +449 -0
- package/docs/examples/gallery/temperate-city-night.png +0 -0
- package/docs/examples/gallery/temperate-city-night.svg +3511 -0
- package/docs/examples/gallery/temperate-city.png +0 -0
- package/docs/examples/gallery/temperate-city.svg +3511 -0
- package/docs/examples/gallery/temperate-village.png +0 -0
- package/docs/examples/gallery/temperate-village.svg +3227 -0
- package/docs/examples/gallery/tropical-city.png +0 -0
- package/docs/examples/gallery/tropical-city.svg +3508 -0
- package/docs/examples/gallery/tropical-village.png +0 -0
- package/docs/examples/gallery/tropical-village.svg +3761 -0
- package/docs/examples/gallery/tundra-city.png +0 -0
- package/docs/examples/gallery/tundra-city.svg +3509 -0
- package/docs/examples/gallery/tundra-village.png +0 -0
- package/docs/examples/gallery/tundra-village.svg +1549 -0
- package/docs/fmg-embed-zoom.md +54 -0
- package/docs/gallery.md +139 -0
- package/docs/geojson.md +188 -0
- package/docs/getting-started.md +157 -0
- package/docs/landscape-character-review.md +55 -0
- package/docs/releases/3.0.1.md +18 -0
- package/docs/scene-schema.md +160 -0
- package/docs/schema-v3.md +38 -0
- package/docs/skins.md +52 -0
- package/docs/test-urls.md +39 -0
- package/docs/url-api.md +252 -0
- package/docs/water-context-v1.md +384 -0
- package/package.json +15 -8
- package/scripts/check-docs.mjs +106 -0
- package/scripts/generate-copperline.mjs +255 -0
- package/scripts/generate-examples.mjs +54 -0
- package/scripts/render-copperline.mjs +36 -0
- package/src/output/geojson-builder.ts +1 -1
- package/symbols/copperline/README.md +10 -3
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Water context v1 — implementation contract
|
|
2
|
+
|
|
3
|
+
Supported by the **3.0.x village library**, for populations 1–1,000, through
|
|
4
|
+
both direct generation and the compressed URL input. This is the normative
|
|
5
|
+
contract for coverage, precision, precedence and diagnostics. City calls reject
|
|
6
|
+
`waterContext`; legacy city polygons remain in city-local units.
|
|
7
|
+
|
|
8
|
+
The hosted renderer is a separate deployment. A host must display the library's
|
|
9
|
+
errors and warnings and verify the deployed version before enabling measured
|
|
10
|
+
inputs. See the [URL adapter contract](url-api.md) and [library API](api.md).
|
|
11
|
+
|
|
12
|
+
## Scope and wire format
|
|
13
|
+
|
|
14
|
+
This implementation serves **villages, population 1–1000**. Add `waterContext`
|
|
15
|
+
to `AzgaarBurgInput` in a direct library call or the compressed `i=` payload. The enclosing URL payload
|
|
16
|
+
version remains `1`; the new object has its own version discriminator.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
type WaterContextV1 =
|
|
20
|
+
| {
|
|
21
|
+
version: 1;
|
|
22
|
+
status: 'measured';
|
|
23
|
+
coordinateSpace: 'burg-local-metres';
|
|
24
|
+
surveyRadiusM: number;
|
|
25
|
+
geometryErrorM: number; // declared maximum physical boundary approximation error, <= 0.5 m
|
|
26
|
+
omittedRivers?: Array<{ riverId?: string; reason: 'local-width-unavailable' }>;
|
|
27
|
+
bodies: Array<{
|
|
28
|
+
featureId?: string; // actual water feature, not the adjacent land feature
|
|
29
|
+
kind: 'ocean' | 'lake';
|
|
30
|
+
distanceM: number;
|
|
31
|
+
bearingDeg: number;
|
|
32
|
+
polygonIndices?: number[]; // indices into coastlineGeometry for this body
|
|
33
|
+
coastApproximation?: 'simple-local-coast'; // explicit opt-in; ocean only
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
version: 1;
|
|
38
|
+
status: 'unknown-units';
|
|
39
|
+
sourceUnit: string;
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Origin is the **burg's actual position**, not its containing cell centre.
|
|
44
|
+
Positive x is east, positive y is south; bearings are clockwise from north.
|
|
45
|
+
`distanceM` is the shortest horizontal distance to a real shoreline segment;
|
|
46
|
+
`bearingDeg` points from the burg to that same closest shoreline point.
|
|
47
|
+
At exactly zero distance, use the local seaward/waterward normal as the bearing.
|
|
48
|
+
Distances must be finite and nonnegative; bearings must be finite in `[0, 360)`;
|
|
49
|
+
the measured survey radius must be finite and at least 3000 m. Unknown versions,
|
|
50
|
+
malformed objects and unsupported coordinate spaces are errors, not legacy input.
|
|
51
|
+
`geometryErrorM` must be finite, nonnegative and no greater than 0.5 m. It describes
|
|
52
|
+
approximation of the authoritative map boundary, not the real-world survey
|
|
53
|
+
accuracy of that boundary. `coastApproximation` is invalid for a lake.
|
|
54
|
+
|
|
55
|
+
When polygons are supplied, `polygonIndices` associates a body's summary with
|
|
56
|
+
its own polygons. This avoids comparing an ocean distance with a nearby river
|
|
57
|
+
bank. Indices must be distinct nonnegative integers within the polygon array;
|
|
58
|
+
one polygon index belongs to at most one ocean/lake body. Include references
|
|
59
|
+
for each reported body intersecting the surveyed disc; a missing reference is a
|
|
60
|
+
`water-context-conflict`. Farther bodies can omit references. Unreferenced water
|
|
61
|
+
polygons remain authoritative geometry (for example, a legacy river polygon),
|
|
62
|
+
but must not be used to validate an unrelated ocean/lake summary.
|
|
63
|
+
|
|
64
|
+
Feature summaries do not claim to describe a whole lake shape. Existing
|
|
65
|
+
`coastlineGeometry` supplies filled polygons; existing `rivers` supplies village
|
|
66
|
+
river surveys. In measured village mode both remain burg-local metres.
|
|
67
|
+
|
|
68
|
+
## 1. Coverage without knowing the final frame
|
|
69
|
+
|
|
70
|
+
**FMG's default survey radius is 3000 metres**, centred on the burg. This is an
|
|
71
|
+
explicit protocol requirement, independent of population-based layout heuristics,
|
|
72
|
+
seed, harbour cells and FMG's grid spacing. A caller may survey a larger radius.
|
|
73
|
+
|
|
74
|
+
FMG must identify every ocean/lake water area intersecting that disc, including
|
|
75
|
+
any body containing the burg. `bodies: []` means none intersects it. Distance to
|
|
76
|
+
a water-cell centre is not a valid inclusion/exclusion test. Use actual feature
|
|
77
|
+
boundaries/intersection tests. If the burg itself is in water, explicit water polygons are required; a
|
|
78
|
+
land-origin distance/bearing approximation is not sufficient. Bodies farther
|
|
79
|
+
away may be listed as additional context, but their presence does not enlarge the survey or the rendered frame.
|
|
80
|
+
|
|
81
|
+
### Authoritative geometry and precision
|
|
82
|
+
|
|
83
|
+
FMG must derive inclusion, distance, bearing and exported polygons from **the
|
|
84
|
+
same authoritative shoreline representation used to display/save the map**.
|
|
85
|
+
That representation includes coastline smoothing, seeded detail and rounding;
|
|
86
|
+
it is not interchangeable with raw packed-cell/Voronoi edges. Hidden layers
|
|
87
|
+
still have geographic geometry. Use a shared geometry service, not the presence
|
|
88
|
+
of currently visible SVG elements, as the source for both display and export.
|
|
89
|
+
|
|
90
|
+
For a loaded map, retain the saved displayed boundary as authoritative until an
|
|
91
|
+
explicit geometry edit/regeneration replaces it. Do not silently regenerate a
|
|
92
|
+
different shore just to export a burg. If unrounded curves become authoritative
|
|
93
|
+
in a later FMG change, display, save and export must all adopt them together.
|
|
94
|
+
|
|
95
|
+
Flatten curves adaptively with a physical error bound in metres. Allocate the
|
|
96
|
+
reported `geometryErrorM` budget across flattening, simplification, Boolean
|
|
97
|
+
operations and final coordinate rounding; the combined maximum boundary deviation
|
|
98
|
+
within coverage must remain <= 0.5 m and preserve topology. Fixed subdivisions
|
|
99
|
+
per curve are not an error bound. Perform survey intersection tests on the curves
|
|
100
|
+
or conservatively against their bounded approximation: an arc can enter the survey
|
|
101
|
+
disc even when its endpoints and control-cell centres are outside it.
|
|
102
|
+
|
|
103
|
+
Calculate scalar summaries from the **final serialized geometry's true shoreline**
|
|
104
|
+
where supplied, using its same body associations. For summaries whose real shore
|
|
105
|
+
lies outside the exported coverage, measure the same canonical full boundary;
|
|
106
|
+
do not compare that value against an artificial crop edge. The 1 m conflict
|
|
107
|
+
tolerance is only an internal payload-consistency check. It does not claim that
|
|
108
|
+
FMG's underlying fantasy-map coast is positioned to metre accuracy. For Borteland,
|
|
109
|
+
a 0.1-map-unit coordinate rounding step corresponds to 482.8 m; mixing rounded
|
|
110
|
+
and unrounded sources cannot be repaired by treating the difference as noise.
|
|
111
|
+
|
|
112
|
+
Use stable feature/polygon ordering. Cache the shared boundary and its spatial
|
|
113
|
+
index, invalidating them on map load, coastline edits, coastline-generation
|
|
114
|
+
settings changes and relevant geometry changes. Do not rescan every world feature
|
|
115
|
+
independently for every burg export.
|
|
116
|
+
|
|
117
|
+
### Filled water versus real shores
|
|
118
|
+
|
|
119
|
+
For a geometry-backed survey, FMG supplies the complete union of ocean/lake water
|
|
120
|
+
inside the survey disc in `coastlineGeometry`. It may include full feature rings
|
|
121
|
+
or clip them against the surrounding square `[-R, R] × [-R, R]`, where
|
|
122
|
+
`R = surveyRadiusM`. That square encloses the disc. Preserve islands/land when
|
|
123
|
+
constructing the water union; do not encode holes as additional filled water
|
|
124
|
+
rings. The existing polygon format does not interpret rings as holes. Decompose
|
|
125
|
+
water around land holes into simple filled pieces when needed.
|
|
126
|
+
|
|
127
|
+
Settlemaker must preserve the **filled union** for water classification and use
|
|
128
|
+
its **exposed real shoreline** for shoreline strokes, nearest-bank/distance
|
|
129
|
+
queries, road setbacks, bridge landfalls, shore walks and dock placement. Filling
|
|
130
|
+
each simple piece is permitted only if the raster result has no gaps; stroking
|
|
131
|
+
each piece separately is not. Dissolve shared/overlapping internal boundaries
|
|
132
|
+
or preserve equivalent boundary provenance through geometry operations. Retain
|
|
133
|
+
land islands as holes in the resulting union. Body-specific summary checks use
|
|
134
|
+
exposed boundary segments attributable to that body's `polygonIndices`; internal
|
|
135
|
+
piece edges and edges hidden under other water are not shores. Do not match a
|
|
136
|
+
body summary to an unrelated river bank.
|
|
137
|
+
|
|
138
|
+
The declared survey-square closure is artificial even where it is exposed in
|
|
139
|
+
the clipped union. Exclude it from every real-bank query; do not just hide its
|
|
140
|
+
stroke. FMG must clip at the declared square when using this cropping convention,
|
|
141
|
+
not at arbitrary unmarked interior edges. A body covering the entire survey can
|
|
142
|
+
have no real shoreline within coverage. It still fills the region with water;
|
|
143
|
+
its crop must not become an apparent nearby coast, and the burg origin must not
|
|
144
|
+
be moved to manufacture a land site. A burg strictly inside water fails with
|
|
145
|
+
`water-context-conflict`; a burg exactly on a real shore is permitted.
|
|
146
|
+
|
|
147
|
+
These rules apply also where river-mouth polygons overlap ocean polygons. All
|
|
148
|
+
water must be combined consistently for physical bank queries; a seam between
|
|
149
|
+
water sources must not become a bridge endpoint or a road setback obstacle.
|
|
150
|
+
|
|
151
|
+
Settlemaker's responsibilities in measured mode:
|
|
152
|
+
|
|
153
|
+
1. Bound water-dependent planning to the surveyed disc, including any detour or
|
|
154
|
+
apron search. Do not infer dry ground outside it from absent polygons.
|
|
155
|
+
2. Keep every final frame corner within `surveyRadiusM - 1000` metres of the
|
|
156
|
+
original burg origin. This leaves a 1 km planning/geometry margin; it does not
|
|
157
|
+
set a minimum frame size or pull distant water into view. Artificial polygon
|
|
158
|
+
closure along the survey square stays outside the frame and margin.
|
|
159
|
+
3. Validate the actual frame and planning bounds at runtime. If they cannot fit,
|
|
160
|
+
return `water-coverage-exceeded` with `requiredSurveyRadiusM`, rounded up to
|
|
161
|
+
the next 1000 m and sufficient for the larger planning bound and frame margin.
|
|
162
|
+
Do not silently crop occupied buildings, treat unsurveyed ground as land, or
|
|
163
|
+
retry with a population-based coastline. Show the coverage error and required
|
|
164
|
+
radius inside Settlemaker. A library caller or an explicit manual workflow can
|
|
165
|
+
supply a larger survey and rebuild the URL. **FMG does not automatically
|
|
166
|
+
resurvey in v1**; no preflight, iframe result transport or new endpoint is
|
|
167
|
+
provided. See section 4 for the actual URL consumer's error behavior.
|
|
168
|
+
|
|
169
|
+
The current 112-case village review has a largest frame-corner radius of about
|
|
170
|
+
601 m. This supports a generous 3 km initial survey; it is not a proof that every
|
|
171
|
+
possible input fits. The runtime check is the guarantee.
|
|
172
|
+
|
|
173
|
+
`surveyRadiusM` describes coverage, never zoom level. A measured shoreline 11 km
|
|
174
|
+
away must remain off a normal hamlet tile.
|
|
175
|
+
|
|
176
|
+
## 2. Input precedence
|
|
177
|
+
|
|
178
|
+
Validate the mode first. Invalid/unknown-version contexts must not quietly fall
|
|
179
|
+
through to legacy behavior.
|
|
180
|
+
|
|
181
|
+
| Input | Required interpretation |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `waterContext` absent | Existing contract unchanged: valid nonempty `coastlineGeometry`, otherwise legacy `oceanBearing` fallback. Existing `rivers` remain additive. |
|
|
184
|
+
| `status: 'unknown-units'` | Suppress geographic ocean/lake fallback and report `water-units-unknown`. Do not pretend a physical survey occurred. Reject simultaneous metre-valued `coastlineGeometry` or `rivers` in this mode; a sender with a real conversion must use `measured`. |
|
|
185
|
+
| Measured context **and `coastlineGeometry` property present** | Those polygons are authoritative ocean/lake geometry for the survey. `[]` explicitly means no ocean/lake water in the surveyed disc. Never generate additional shores from body summaries or legacy bearings. |
|
|
186
|
+
| Measured context, polygon property omitted, `bodies: []` | Authoritatively no ocean/lake within the surveyed disc. Suppress `oceanBearing`; do not invent water outside the survey. |
|
|
187
|
+
| Measured context, polygon property omitted, one relevant ocean summary explicitly declaring `coastApproximation: "simple-local-coast"` and satisfying the eligibility rules below | Generate an approximate shoreline anchored at its measured nearest point/distance. No population-derived shore standoff. Mark the result `approximate`. |
|
|
188
|
+
| Measured context, polygon property omitted, any relevant lake, multiple bodies, complex coast, or ocean without the explicit eligible approximation declaration | Return `water-geometry-required`. Feature count alone cannot determine local shoreline topology. |
|
|
189
|
+
|
|
190
|
+
**Simple-coast eligibility is geometric, not a feature count.** FMG may declare
|
|
191
|
+
`simple-local-coast` only after inspecting the canonical coast across the survey:
|
|
192
|
+
there must be one simple open shoreline section, with the burg on land, one
|
|
193
|
+
landward side and one waterward side, and no nearby opposite shoreline. Islands,
|
|
194
|
+
peninsulas, multiple nearby shoreline sections, enclosed bays with opposing banks,
|
|
195
|
+
and other complex coast configurations require polygons even if they all belong
|
|
196
|
+
to one connected ocean feature. If FMG cannot establish eligibility, it sends
|
|
197
|
+
polygons; omission of the opt-in never authorizes guessing. Settlemaker validates
|
|
198
|
+
its structural prerequisites and relies on FMG's explicit geographic declaration.
|
|
199
|
+
|
|
200
|
+
For an accepted approximate coast, the global minimum distance to the **whole
|
|
201
|
+
generated shoreline**, including the interior of curved segments, must not be
|
|
202
|
+
less than the reported distance (apart from numerical tolerance <= 1 m). Checking
|
|
203
|
+
only the nearest-point anchor or sampled control vertices is insufficient. Seeded
|
|
204
|
+
detail must remain waterward of that constraint. Approximation can change the
|
|
205
|
+
shape of a simple coast, but cannot move water toward the burg.
|
|
206
|
+
|
|
207
|
+
A body whose nearest water is outside the bounded local planning area does not
|
|
208
|
+
need a generated shoreline. Omit its off-map geometry without changing the frame.
|
|
209
|
+
|
|
210
|
+
**Geometry beats scalar summaries.** If a polygon disagrees with its summary,
|
|
211
|
+
use the polygon without translation, scaling or a second generated shoreline,
|
|
212
|
+
and report `water-context-conflict`. Recompute visible-water and proximity facts
|
|
213
|
+
from the polygon. Differences within 1 m may be treated as numerical tolerance;
|
|
214
|
+
larger differences must be reported. A missing geometric feature in a purported
|
|
215
|
+
complete polygon survey is also a conflict, not permission to invent it.
|
|
216
|
+
|
|
217
|
+
`rivers` remain additive and are not erased by an empty **ocean/lake** survey.
|
|
218
|
+
Do not send a river twice through `rivers` and `coastlineGeometry`; include river
|
|
219
|
+
polygons in the latter only when that river is not also supplied as a survey.
|
|
220
|
+
`port` and `harbourSize` control infrastructure, never water visibility or distance.
|
|
221
|
+
A port without an accessible nearby real shoreline produces a nonfatal
|
|
222
|
+
`water-context-conflict` and no dock/jetty placement. Keep the settlement and water
|
|
223
|
+
at their supplied locations; do not pull a coast inward to satisfy `port`.
|
|
224
|
+
|
|
225
|
+
## 3. Physical units and cities
|
|
226
|
+
|
|
227
|
+
FMG performs exactly one world-to-metre conversion:
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
metresPerMapUnit = distanceScale × kilometresPerSelectedUnit × 1000
|
|
231
|
+
localX = (worldX - burg.x) × metresPerMapUnit
|
|
232
|
+
localY = (worldY - burg.y) × metresPerMapUnit
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use the selected unit, not an assumed kilometre scale. For the supplied Borteland
|
|
236
|
+
save: `3 × 1.609344 × 1000 = 4828.032` metres per map unit. Apply this same factor
|
|
237
|
+
to coordinates and lengths actually measured in map units, including shore
|
|
238
|
+
distances, polygon coordinates and coordinate-derived local river-bank spacing.
|
|
239
|
+
Do not apply it to values already stored in physical units.
|
|
240
|
+
|
|
241
|
+
**FMG `River.width` is mouth width in kilometres.** Convert that scalar with
|
|
242
|
+
`widthM = River.width * 1000`, independent of the selected map distance unit or
|
|
243
|
+
scale. `RiverReading.widthKm` has the same physical-unit meaning. Neither value
|
|
244
|
+
establishes the river width beside an upstream burg. Prefer local bank geometry
|
|
245
|
+
from the authoritative river representation, transformed consistently into
|
|
246
|
+
metres. Changing the display unit must not rescale an already kilometre-valued
|
|
247
|
+
river width.
|
|
248
|
+
|
|
249
|
+
Do not substitute mouth width for a missing local width silently. For measured
|
|
250
|
+
water v1, a `rivers` survey's `widthM` must be a locally established width; use
|
|
251
|
+
explicit local river polygons where the channel varies materially across the
|
|
252
|
+
village. If only mouth width is available, omit the unsupported local survey and
|
|
253
|
+
show a nonfatal `water-river-local-width-unavailable` warning inside Settlemaker.
|
|
254
|
+
FMG communicates that omission through optional `waterContext.omittedRivers`,
|
|
255
|
+
an array of `{ riverId?: string, reason: 'local-width-unavailable' }`. This is
|
|
256
|
+
not a declaration that the surrounding region has no river. Mouth-width-only
|
|
257
|
+
rendering, if introduced later, needs an explicit approximation contract.
|
|
258
|
+
|
|
259
|
+
For a custom unit, FMG must have a positive, explicit metres-per-unit conversion.
|
|
260
|
+
If none exists, send `status: 'unknown-units'` with its label. Omit metric geometry
|
|
261
|
+
and distance assertions. This is neither a zero-distance shore nor an empty
|
|
262
|
+
measured survey. Settlemaker must display the units diagnostic itself so the
|
|
263
|
+
iframe user can see why geographic water is unavailable. Do not silently assume custom units
|
|
264
|
+
are kilometres. Audit each measurement by its source unit; kilometre-valued
|
|
265
|
+
context windows and corridor distances also need the selected-unit conversion
|
|
266
|
+
when their underlying measurements were made in map coordinates.
|
|
267
|
+
|
|
268
|
+
**City scaling is owned by Settlemaker, not guessed by FMG.** FMG's future measured
|
|
269
|
+
city payload must also use metres. A city implementation must establish and
|
|
270
|
+
publish its physical `metresPerMeshUnit`, use it consistently for water, buildings,
|
|
271
|
+
roads, frame and output, and define its own survey envelope. A viewBox, SVG pixel
|
|
272
|
+
scale, Voronoi radius or `urbanDensity` is not that conversion.
|
|
273
|
+
|
|
274
|
+
Until that work exists, `waterContext` on population >1000 returns
|
|
275
|
+
`water-context-unsupported-engine`; it must never reinterpret metres as mesh
|
|
276
|
+
units. FMG does not enable this new mode for cities yet. Legacy city URLs keep
|
|
277
|
+
their existing contract and do not gain a geographic-accuracy guarantee from this
|
|
278
|
+
village rollout. This restriction is deliberate and visible, not a silent no-op.
|
|
279
|
+
|
|
280
|
+
## 4. Output, transport and rollout requirements
|
|
281
|
+
|
|
282
|
+
**V1 remains URL-only. There is no automatic FMG resurvey or recovery.** FMG
|
|
283
|
+
cannot read a cross-origin iframe's library result. Do not depend on iframe DOM
|
|
284
|
+
access, polling its JavaScript, or a result listener that has not been specified.
|
|
285
|
+
No `postMessage` protocol is introduced by this contract.
|
|
286
|
+
|
|
287
|
+
Settlemaker must display errors and nonfatal warnings visibly **inside both the
|
|
288
|
+
embedded preview and the external-open page**. Errors replace the failed map;
|
|
289
|
+
nonfatal warnings accompany the rendered map. They must be readable on initial
|
|
290
|
+
load without hover, clicking or other iframe interaction: FMG's preview can have
|
|
291
|
+
pointer events disabled. Coverage errors include the required survey radius in
|
|
292
|
+
the visible explanation. Unknown units and omitted local river widths must be
|
|
293
|
+
identified as unavailable data, not presented as a verified dry landscape.
|
|
294
|
+
|
|
295
|
+
Also expose a machine-readable `waterContextResult` through the library generation
|
|
296
|
+
result for direct consumers and Settlemaker's own UI:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
interface WaterContextResult {
|
|
300
|
+
version: 1;
|
|
301
|
+
status: 'measured' | 'approximate' | 'unknown-units' | 'error';
|
|
302
|
+
issues: Array<{
|
|
303
|
+
code: 'water-coverage-exceeded' | 'water-geometry-required'
|
|
304
|
+
| 'water-context-conflict' | 'water-units-unknown'
|
|
305
|
+
| 'water-context-unsupported-engine' | 'water-context-invalid'
|
|
306
|
+
| 'water-river-local-width-unavailable';
|
|
307
|
+
requiredSurveyRadiusM?: number;
|
|
308
|
+
}>;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`approximate` identifies a measured-distance ocean without explicit bank polygons.
|
|
313
|
+
A geometry conflict is an issue on a geometry-backed result, not permission to
|
|
314
|
+
switch modes. Coverage, required-geometry, unsupported-engine and invalid-context
|
|
315
|
+
errors do not return a falsely complete rendered map. Legacy input omits this
|
|
316
|
+
new result entirely. Fatal errors throw `WaterContextError`, carrying this result on
|
|
317
|
+
`error.waterContextResult`. Successful `generateSettlement` calls expose it as
|
|
318
|
+
`result.waterContextResult`; village models also expose it on the site. It does
|
|
319
|
+
not introduce an iframe message protocol or an HTTP API.
|
|
320
|
+
|
|
321
|
+
The host must preserve `waterContext` when building links. If compression is
|
|
322
|
+
unavailable or the encoded payload exceeds the URL budget, do **not** fall back
|
|
323
|
+
to a flat URL that drops measured/unknown state and restores `oceanBearing`.
|
|
324
|
+
Simplify polygons within the declared physical error budget without changing
|
|
325
|
+
local water topology or closing narrow channels, recompute summaries from the
|
|
326
|
+
final serialized boundary, and revalidate coverage/precision/URL size; otherwise report
|
|
327
|
+
that the measured preview cannot be encoded. The existing flat tier has no
|
|
328
|
+
representation for this mode.
|
|
329
|
+
|
|
330
|
+
Roll out in this order:
|
|
331
|
+
|
|
332
|
+
1. Implement and test the Settlemaker decoder, water preparation, bounded planning,
|
|
333
|
+
precedence, canonical/union-boundary queries, precision, visible diagnostics
|
|
334
|
+
and output semantics described here.
|
|
335
|
+
2. Publish/pin a release explicitly supporting **water-context v1 for villages**.
|
|
336
|
+
3. Enable FMG's measured village payload against that release. Verify both preview
|
|
337
|
+
and external-open URLs. Keep city and legacy paths separate during rollout.
|
|
338
|
+
|
|
339
|
+
Older decoders ignore unrecognized fields; therefore merely adding this object
|
|
340
|
+
to an older deployed renderer's URL is **not** a compatibility check. Do not enable the FMG
|
|
341
|
+
sender before the supporting renderer is deployed.
|
|
342
|
+
|
|
343
|
+
## 5. Shared acceptance fixtures
|
|
344
|
+
|
|
345
|
+
Both implementations must test:
|
|
346
|
+
|
|
347
|
+
- Tarrimas-Ha: approximately 11,115 m to sea → no sea in the hamlet frame, with
|
|
348
|
+
either an empty complete 3 km survey or that distant body listed additionally.
|
|
349
|
+
- Same burg with a 40 m shore → visible water at that physical location.
|
|
350
|
+
- A nearby portless beach → water without invented harbour infrastructure.
|
|
351
|
+
- Nearby finite lake geometry versus a distant lake in the FMG neighbourhood.
|
|
352
|
+
- Equivalent maps in km, miles and nautical miles; custom units with and without
|
|
353
|
+
an explicit conversion.
|
|
354
|
+
- Measured empty survey plus legacy bearing → no generated sea.
|
|
355
|
+
- Explicit polygon, omitted polygons and explicit `[]` as separate cases; polygon
|
|
356
|
+
versus summary conflict → polygon retained with a conflict issue.
|
|
357
|
+
- Ocean/lake emptiness does not erase an independently supplied river.
|
|
358
|
+
- Survey too small for the protocol, and a valid survey exceeded by actual layout
|
|
359
|
+
or planning → explicit errors, never a falsely dry region.
|
|
360
|
+
- Measured lake without sufficient geometry → `water-geometry-required`.
|
|
361
|
+
- New context on a city → unsupported-engine error; legacy city output unchanged.
|
|
362
|
+
- URL-budget/compression fallback cannot discard measured or unknown state.
|
|
363
|
+
- No-context URLs retain existing compatibility behavior.
|
|
364
|
+
- Real iframe and external-open flows for coverage errors, unknown units, geometry
|
|
365
|
+
conflicts and missing local river width: diagnostics visible without pointer
|
|
366
|
+
interaction; no implicit automatic resurvey. Library-only tests do not satisfy
|
|
367
|
+
this acceptance case.
|
|
368
|
+
- A curve with endpoints outside the survey but an interior arc entering it;
|
|
369
|
+
hidden coastlines, coastline edits/settings changes and an old loaded save.
|
|
370
|
+
Measurement and polygon export use the same displayed/saved geometry, within
|
|
371
|
+
the declared approximation budget after final serialization.
|
|
372
|
+
- An island surrounded by one ocean decomposed into filled pieces, a lake with an
|
|
373
|
+
island, overlapping river-mouth/ocean polygons, and water covering the entire
|
|
374
|
+
survey: no filled land island, internal stroke, crop-edge bank, false bridge
|
|
375
|
+
landfall or relocation of the burg origin.
|
|
376
|
+
- Peninsula and island cases with one ocean feature but multiple local shores
|
|
377
|
+
require polygons. Missing simple-coast opt-in fails; an eligible approximation
|
|
378
|
+
satisfies its minimum-distance bound along the whole generated curve.
|
|
379
|
+
- An upstream burg with a much wider river mouth: local geometry controls local
|
|
380
|
+
width. Converting kilometre-valued mouth width uses only `* 1000`; changing
|
|
381
|
+
display units does not change the value. Unavailable local width is reported.
|
|
382
|
+
- `port` with no accessible nearby shore: nonfatal conflict, no invented docks.
|
|
383
|
+
- Population 1000 versus 1001, malformed body/polygon references, and a URL
|
|
384
|
+
simplification attempt that would close a narrow land neck or water channel.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "settlemaker",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Procedural village and city map engine with SVG, local GeoJSON, biome artwork and portable skins for Node.js and browsers",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
"build:art": "node scripts/build-runtime-art.mjs",
|
|
13
13
|
"review:art": "npm run build && node scripts/review-artwork.mjs",
|
|
14
14
|
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
15
|
-
"build:lib": "esbuild src/index.ts --bundle --format=esm --target=es2022 --minify --outfile=dist/settlemaker.browser.js --banner:js=\"/* settlemaker
|
|
15
|
+
"build:lib": "esbuild src/index.ts --bundle --format=esm --target=es2022 --minify --outfile=dist/settlemaker.browser.js --banner:js=\"/* settlemaker \u2014 GPL-3.0-only. Corresponding source: https://github.com/barrulus/settlemaker */\"",
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"test:watch": "vitest",
|
|
18
18
|
"review:roads": "node scripts/run-road-review.mjs",
|
|
19
19
|
"measure:city-glyphs": "esbuild scripts/measure-city-glyphs.ts --bundle --platform=node --format=cjs --packages=external --outfile=output/measure-city-glyphs.cjs && node output/measure-city-glyphs.cjs",
|
|
20
|
-
"review:cities": "esbuild scripts/review-cities.ts --bundle --platform=node --format=cjs --packages=external --outfile=output/city-review-runner.cjs && node output/city-review-runner.cjs"
|
|
20
|
+
"review:cities": "esbuild scripts/review-cities.ts --bundle --platform=node --format=cjs --packages=external --outfile=output/city-review-runner.cjs && node output/city-review-runner.cjs",
|
|
21
|
+
"docs:images": "npm run build && node scripts/generate-examples.mjs",
|
|
22
|
+
"docs:check": "npm run build && npm run build:lib && node scripts/check-docs.mjs"
|
|
21
23
|
},
|
|
22
24
|
"license": "GPL-3.0-only",
|
|
23
25
|
"author": "barrulus",
|
|
@@ -43,9 +45,6 @@
|
|
|
43
45
|
"scripts/build-runtime-art.mjs",
|
|
44
46
|
"scripts/art",
|
|
45
47
|
"NOTICE",
|
|
46
|
-
"docs/releases/3.0.0.md",
|
|
47
|
-
"docs/skins.md",
|
|
48
|
-
"docs/current-symbol-library.md",
|
|
49
48
|
"docs/skins.schema.json",
|
|
50
49
|
"docs/examples/moon-glass.skin.json",
|
|
51
50
|
"docs/examples/copperline.skin.json",
|
|
@@ -59,7 +58,15 @@
|
|
|
59
58
|
"symbols/landscape",
|
|
60
59
|
"symbols/greens",
|
|
61
60
|
"symbols/infrastructure",
|
|
62
|
-
"symbols/markers"
|
|
61
|
+
"symbols/markers",
|
|
62
|
+
"docs/*.md",
|
|
63
|
+
"docs/releases/3.0.0.md",
|
|
64
|
+
"docs/releases/3.0.1.md",
|
|
65
|
+
"docs/examples/gallery",
|
|
66
|
+
"scripts/generate-examples.mjs",
|
|
67
|
+
"scripts/check-docs.mjs",
|
|
68
|
+
"scripts/generate-copperline.mjs",
|
|
69
|
+
"scripts/render-copperline.mjs"
|
|
63
70
|
],
|
|
64
71
|
"dependencies": {
|
|
65
72
|
"@types/geojson": "^7946.0.14",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Check public guidance against real files, declarations and compiled examples. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import ts from 'typescript';
|
|
9
|
+
import { SETTLEMAKER_VERSION, SKIN_SLOTS, decodeBurgParam } from '../dist/index.js';
|
|
10
|
+
|
|
11
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
13
|
+
const read = path => readFileSync(resolve(root, path), 'utf8');
|
|
14
|
+
const pages = ['README.md', 'docs/README.md', 'docs/getting-started.md', 'docs/api.md',
|
|
15
|
+
'docs/geojson.md', 'docs/scene-schema.md', 'docs/url-api.md', 'docs/water-context-v1.md',
|
|
16
|
+
'docs/skins.md', 'docs/current-symbol-library.md', 'docs/artwork-integration.md',
|
|
17
|
+
'docs/fmg-embed-zoom.md', 'docs/development.md', 'docs/test-urls.md', 'docs/gallery.md',
|
|
18
|
+
'docs/schema-v3.md', 'symbols/copperline/README.md', `docs/releases/${pkg.version}.md`];
|
|
19
|
+
const withoutCode = text => text.replace(/```[^\n]*\n[\s\S]*?```/g, '');
|
|
20
|
+
function anchors(path) {
|
|
21
|
+
const counts = new Map();
|
|
22
|
+
return new Set([...withoutCode(read(path)).matchAll(/^#{1,6}\s+(.+)$/gm)].map(([, heading]) => {
|
|
23
|
+
const slug = heading.toLowerCase().replace(/[^\p{L}\p{N}_\-\s]/gu, '').replace(/\s/g, '-');
|
|
24
|
+
const count = counts.get(slug) ?? 0;
|
|
25
|
+
counts.set(slug, count + 1);
|
|
26
|
+
return count ? `${slug}-${count}` : slug;
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
let links = 0;
|
|
30
|
+
for (const page of pages) {
|
|
31
|
+
const text = withoutCode(read(page));
|
|
32
|
+
const targets = [...text.matchAll(/\]\(([^\s)]+)(?:\s+"[^"]*")?\)/g), ...text.matchAll(/(?:src|href)="([^"]+)"/g)];
|
|
33
|
+
for (const [, target] of targets) {
|
|
34
|
+
let local;
|
|
35
|
+
if (target.startsWith('https://github.com/barrulus/settlemaker/blob/')) {
|
|
36
|
+
const match = target.match(/^https:\/\/github\.com\/barrulus\/settlemaker\/blob\/(v[^/]+)\/(.+)$/);
|
|
37
|
+
assert.ok(match, `Invalid source link in ${page}: ${target}`);
|
|
38
|
+
assert.equal(match[1], `v${pkg.version}`, `Wrong documentation tag in ${page}`);
|
|
39
|
+
local = resolve(root, match[2]);
|
|
40
|
+
} else if (target.startsWith('https://raw.githubusercontent.com/barrulus/settlemaker/')) {
|
|
41
|
+
const match = target.match(/^https:\/\/raw\.githubusercontent\.com\/barrulus\/settlemaker\/(v[^/]+)\/(.+)$/);
|
|
42
|
+
assert.ok(match, `Invalid image link in ${page}: ${target}`);
|
|
43
|
+
assert.equal(match[1], `v${pkg.version}`, `Wrong image tag in ${page}`);
|
|
44
|
+
local = resolve(root, match[2]);
|
|
45
|
+
} else if (/^(?:https?:|mailto:)/.test(target)) continue;
|
|
46
|
+
else local = target.startsWith('#') ? resolve(root, page) + target : resolve(root, dirname(page), target);
|
|
47
|
+
const [path, anchor] = local.split('#');
|
|
48
|
+
assert.ok(existsSync(decodeURIComponent(path)), `Broken link in ${page}: ${target}`);
|
|
49
|
+
if (anchor && path.endsWith('.md')) assert.ok(anchors(path).has(decodeURIComponent(anchor)), `Broken anchor in ${page}: ${target}`);
|
|
50
|
+
links++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
assert.equal(SETTLEMAKER_VERSION, pkg.version);
|
|
54
|
+
assert.equal(Object.keys(SKIN_SLOTS).length, 693);
|
|
55
|
+
const gallery = JSON.parse(read('docs/examples/gallery/fixtures.json'));
|
|
56
|
+
assert.equal(gallery.generatorVersion, pkg.version, 'Regenerate the gallery for this release');
|
|
57
|
+
for (const fixture of gallery.fixtures) {
|
|
58
|
+
assert.equal(fixture.engine, fixture.burg.population <= 1000 ? 'village' : 'settlement');
|
|
59
|
+
for (const ext of ['svg', 'png']) assert.ok(existsSync(join(root, `docs/examples/gallery/${fixture.id}.${ext}`)));
|
|
60
|
+
if (fixture.previewUrl) {
|
|
61
|
+
assert.ok(!fixture.skin, 'The URL codec does not carry skins');
|
|
62
|
+
assert.deepEqual(await decodeBurgParam(new URL(fixture.previewUrl).searchParams.get('i')), { burg: fixture.burg, seed: fixture.seed });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const work = mkdtempSync(join(tmpdir(), 'settlemaker-docs-'));
|
|
67
|
+
const entry = pathToFileURL(join(root, 'dist/index.js')).href;
|
|
68
|
+
const blocks = (page, language) => [...read(page).matchAll(new RegExp('```' + language + '\\n([\\s\\S]*?)```', 'g'))].map(m => m[1]);
|
|
69
|
+
const imports = code => code.replaceAll("from 'settlemaker'", `from '${entry}'`).replaceAll("import('settlemaker')", `import('${entry}')`)
|
|
70
|
+
.replaceAll("import.meta.resolve('settlemaker/docs/examples/copperline.skin.json')", JSON.stringify(pathToFileURL(join(root, 'docs/examples/copperline.skin.json')).href));
|
|
71
|
+
function run(name, code) {
|
|
72
|
+
const path = join(work, name + '.mjs');
|
|
73
|
+
writeFileSync(path, imports(code));
|
|
74
|
+
try { execFileSync(process.execPath, [path], { cwd: work, stdio: 'pipe' }); }
|
|
75
|
+
catch (error) { throw new Error(`${name}: ${error.message}\n${error.stderr?.toString().slice(-3000) ?? ''}`); }
|
|
76
|
+
}
|
|
77
|
+
const quick = blocks('README.md', 'js');
|
|
78
|
+
run('readme', quick.join('\n') + '\nif (!result.svg.includes("<svg") || !moonVillage.svg.includes("#68748c")) throw new Error("README output missing");');
|
|
79
|
+
for (const [i, code] of blocks('docs/getting-started.md', 'js').entries()) run(`getting-started-${i}`, code);
|
|
80
|
+
const html = blocks('docs/getting-started.md', 'html')[0];
|
|
81
|
+
const browser = html.match(/<script type="module">([\s\S]*?)<\/script>/)[1]
|
|
82
|
+
.replace("'./assets/settlemaker.browser.js'", JSON.stringify(pathToFileURL(join(root, 'dist/settlemaker.browser.js')).href));
|
|
83
|
+
run('browser-page', 'const element = {}; const document = {querySelector: () => element};\n' + browser + '\nif (!element.innerHTML.includes("<svg")) throw new Error("No browser map");');
|
|
84
|
+
const scene = blocks('docs/scene-schema.md', 'js');
|
|
85
|
+
run('scene', scene[0]);
|
|
86
|
+
const burg = scene[0].match(/const burg = \{[\s\S]*?\n\};/)[0];
|
|
87
|
+
run('lower-level', burg + '\n' + scene[1]);
|
|
88
|
+
run('tiles', quick[0] + '\n' + blocks('docs/geojson.md', 'js')[0] + '\nif (!rootTileSvg.includes("<svg")) throw new Error("No tile");');
|
|
89
|
+
const url = blocks('docs/url-api.md', 'js');
|
|
90
|
+
run('url-encode', url[0]);
|
|
91
|
+
run('url-renderer', 'const location = {search: "?name=Docs&pop=300&plaza=1&seed=2&roads=20,140,270"};\n' + url[1] + '\nif (result.kind !== "village") throw new Error("Wrong URL engine");');
|
|
92
|
+
const typeFiles = [];
|
|
93
|
+
for (const page of ['docs/getting-started.md', 'docs/api.md']) {
|
|
94
|
+
for (const [i, code] of blocks(page, 'ts').entries()) {
|
|
95
|
+
const path = join(work, `${page.replaceAll('/', '-')}-${i}.mts`);
|
|
96
|
+
writeFileSync(path, code.replaceAll("from 'settlemaker'", `from '${join(root, 'dist/index.js')}'`));
|
|
97
|
+
typeFiles.push(path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const program = ts.createProgram(typeFiles, { noEmit: true, strict: true, target: ts.ScriptTarget.ES2022,
|
|
101
|
+
module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, types: [] });
|
|
102
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
103
|
+
assert.equal(diagnostics.length, 0, ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
104
|
+
getCanonicalFileName: f => f, getCurrentDirectory: () => work, getNewLine: () => '\n',
|
|
105
|
+
}));
|
|
106
|
+
console.log(`Passed: ${pages.length} public pages, ${links} local links/anchors, ${gallery.fixtures.length} gallery fixtures, README/Node/browser/URL/scene/tile examples and TypeScript snippets.`);
|