@tileflow/core 0.1.0-alpha.33 → 0.1.0-alpha.35

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.
@@ -1,11 +1,15 @@
1
1
  # 3D buildings
2
2
 
3
- `createTileflowBuildings3D` raises the buildings of a Tileflow map's vector tiles round the camera
4
- and lights them from the theme controller's sun: walls and roofs take the light by how they face
5
- it, buildings cast shadows on each other and on the flat map, the ground darkens at the foot of the
6
- walls, and at night flood lights wash the foot of shops, busy places, landmarks and towers while
7
- the places busy at night light their ground floors. Showing, hiding and growing the city lays out
8
- no source, so it works as a 3D switch and as an animated height.
3
+ `createTileflowBuildings3D` raises the buildings of a Tileflow map's vector tiles round the camera.
4
+ By default it preserves the map's building colours and native extrusion lighting (`style.light`
5
+ and its vertical wall gradient). Showing, hiding and changing height are immediate. Cast shadows,
6
+ contact shadow, illuminated outlines, random colour variation and night lights are off.
7
+
8
+ Scene sun lighting, shadows, decorative effects and animated height remain available through
9
+ explicit options. They are independent of the map's trees.
10
+
11
+ Tileflow landmarks and a map's own building models can stand in the same 3D scene, replacing
12
+ the buildings under their footprints.
9
13
 
10
14
  It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
11
15
  labels. It needs WebGL2 and the Mercator projection, which MapLibre's globe turns into from about
@@ -27,26 +31,217 @@ import {
27
31
 
28
32
  export function raiseTheCity(map: TileflowBuildings3DMap, controller: TileflowThemeController) {
29
33
  const buildings = createTileflowBuildings3D({map, controller});
30
- // The buildings slide out of the ground over a second.
31
- void buildings.show({transition: {duration: 1000}});
34
+ // No animation unless requested.
35
+ void buildings.show();
32
36
  return buildings;
33
37
  }
34
38
  ```
35
39
 
36
- `show()` raises the buildings, `hide()` sinks them back into the flat map, and `grow(scale)` eases
37
- them to a share of their height, from 0 to 1. Each returns a promise that resolves once the motion
38
- ends; a newer call takes over from where the buildings stand. A browser preference for reduced
40
+ `show()`, `hide()` and `grow(scale)` change the height immediately by default (duration 0).
41
+ `show({transition: {duration: 800, mode: 'fade'}})` reveals full-height buildings with a
42
+ dithered fade. The default animation mode is `grow`; both modes respect reduced-motion settings.
43
+ To animate, pass `{transition: {duration: 800}}` explicitly; `grow(scale)` accepts a height share
44
+ from 0 to 1. Each returns a promise that resolves once the change ends; a newer call takes over from where the buildings stand. A browser preference for reduced
39
45
  motion changes at once. `dispose()` takes the layer off the map.
40
46
 
41
47
  The renderer reads the source's z15 tiles round the camera (z15 is where World's buildings carry
42
48
  their heights; a source that stops earlier is read at its own maximum zoom), up to `maxTiles`
43
49
  tiles (25 by default), and meshes them a few milliseconds per frame. It reads World's
44
- `render_height`, `render_min_height`, `hide_3d`, `building_tone` and `night_use`, and the places in
45
- World's `poi` layer (see [night light](#night-light)).
50
+ `render_height`, `render_min_height`, `hide_3d`, `height_source`, `building_tone`, `class`,
51
+ `night_use` and the roof fields (see [roofs](#roofs)), and the places in World's `poi` layer (see
52
+ [night light](#night-light)). `stats.declared` tells whether the map's
53
+ style describes its 3D buildings. Recompile existing maps to pick up the new defaults: previously
54
+ compiled metadata with explicit effects enabled continues to honour those values.
55
+
56
+ ## Design the buildings in the map
57
+
58
+ The map describes how its 3D buildings look, in its buildings module, and publishes it with its
59
+ style for every theme; the renderer follows theme changes and blends. The design is visible in
60
+ `tileflow preview` and validated with the rest of the map:
61
+
62
+ <!-- docs:check -->
63
+
64
+ ```ts
65
+ import {defineMap, expr, field, refine, refineRenderTarget} from '@tileflow/core';
66
+ import {streets} from '@tileflow/maps';
67
+
68
+ const floors = expr.divide(expr.toNumber(expr.get(field('renderHeight')), 0), 3.2);
69
+
70
+ export default defineMap({
71
+ id: 'lit-city',
72
+ name: 'Lit city',
73
+ version: 1,
74
+ extends: streets,
75
+ modules: {
76
+ buildings: refine({
77
+ threeDimensional: {
78
+ lighting: 'sun',
79
+ shadows: true,
80
+ occlusion: true,
81
+ edges: 1,
82
+ colorVariation: 0.06,
83
+ windows: true,
84
+ nightLight: 0.6,
85
+ edgeColor: '#2b2a33',
86
+ },
87
+ renderStack: {
88
+ // Core's 3D buildings take the colour of the map's 3D building pass.
89
+ tints: refineRenderTarget({
90
+ target: 'buildings.render.extrusion',
91
+ renderer: 'extrusion',
92
+ style: {
93
+ color: expr.step(expr.modulo(floors, 3), '#e8d9c4', [
94
+ [1, '#d6e2d8'],
95
+ [2, '#e5d4e6'],
96
+ ]),
97
+ },
98
+ }),
99
+ },
100
+ }),
101
+ },
102
+ });
103
+ ```
104
+
105
+ - Colour: each building takes the colour the map gives its 3D buildings: Streets' 3D building pass
106
+ (the theme's `buildings.extrusion`), else the buildings module's extrusion, else its roofs. A
107
+ colour that depends on the building's data is evaluated per building and theme when a tile or the
108
+ set of themes changes, and the themes either side of a blend's position mix on the GPU, so a
109
+ moving blend costs nothing per frame.
110
+ - `threeDimensional.shadows` and `occlusion` (the band at the foot of the walls and the darker tight
111
+ corners) default to false. `windows: true` adds invented night windows, lit by rooms; they are
112
+ decoration, not data, and are off by default. Where World says what a building is used for
113
+ (`class`), sheds, garages, barns, factories, churches and service buildings get no rows of
114
+ windows, homes light more of theirs at night, in a warm light, and offices, schools and hospitals
115
+ fewer, in a cooler one.
116
+ - `threeDimensional.edges` and `edgeColor` set the [edges](#edges), and `nightLight` the
117
+ [night light](#night-light). Numbers and colours may be theme tokens.
118
+
119
+ ## Landmarks
120
+
121
+ Maps on Tileflow World show Tileflow's landmarks, such as the Eiffel Tower, as 3D models in place of
122
+ the buildings under them. They come from a landmarks tileset kept apart from the map's data: each of
123
+ its tiles carries the models standing in it, with their levels of detail and footprints, and each
124
+ region's index names its tiles by their hash. The renderer reads the tileset's TileJSON once the
125
+ buildings show in 3D, then only the indexes of the regions round the camera and the landmark tiles
126
+ near it, so a landmark in Paris costs nothing in Madrid. New and improved landmarks reach every map
127
+ without a new build of the map. A tileset that cannot be read is tried again after five minutes,
128
+ and the buildings stay drawn meanwhile.
129
+
130
+ A map switches them off, or names the TileJSON of another landmarks tileset:
131
+
132
+ <!-- docs:check -->
133
+
134
+ ```ts
135
+ import {defineMap, refine} from '@tileflow/core';
136
+ import {streets} from '@tileflow/maps';
137
+
138
+ export default defineMap({
139
+ id: 'no-landmarks',
140
+ name: 'Streets without landmarks',
141
+ version: 1,
142
+ extends: streets,
143
+ modules: {
144
+ buildings: refine({threeDimensional: {landmarks: false}}),
145
+ },
146
+ });
147
+ ```
148
+
149
+ The map's own building models come first: a landmark with the same id as one of them, or whose
150
+ footprint meets one's, is not drawn.
151
+
152
+ ## Building models
153
+
154
+ A map can place its own 3D models of buildings in the city: a landmark the extruded footprints draw
155
+ badly, or the buildings an application presents. Each model stands where the map says, in the same
156
+ light, shadows and theme as the buildings round it, and the buildings under its footprint are no
157
+ longer raised while it stands. Put glTF binaries (`.glb`) in a directory, declare it in the map's
158
+ `models`, and say where each model stands:
159
+
160
+ <!-- docs:check -->
161
+
162
+ ```ts
163
+ import {defineMap, refine} from '@tileflow/core';
164
+ import {streets, streetsModels} from '@tileflow/maps';
165
+
166
+ export default defineMap({
167
+ id: 'paris-landmarks',
168
+ name: 'Paris landmarks',
169
+ version: 1,
170
+ extends: streets,
171
+ // ./models holds eiffel-tower.glb, and eiffel-tower.mid.glb and eiffel-tower.far.glb for
172
+ // farther views.
173
+ // Keep Streets' tree models alongside the building models.
174
+ models: [streetsModels, './models'],
175
+ modules: {
176
+ buildings: refine({
177
+ threeDimensional: {
178
+ models: [{model: 'eiffel-tower', at: [2.294481, 48.858258], heading: 0}],
179
+ modelColors: {surface: '#7a5c45', accent: '#5b4332'},
180
+ },
181
+ }),
182
+ },
183
+ });
184
+ ```
185
+
186
+ `at` is the longitude and latitude of the model's origin and `heading` the degrees clockwise from
187
+ north its own north is turned to. A map places up to 256 models; one model may stand in several
188
+ places. A placement may instead name a prepared model file served elsewhere with `url`, its HTTPS
189
+ address; the map then needs no `.glb` of its own for it.
190
+
191
+ How to prepare a model:
192
+
193
+ - **Materials name the parts.** Name each material `surface`, `secondary`, `glass`, `accent` or
194
+ `roof` (an exporter's `.001` suffix is ignored); a model whose materials name a tree's parts is a
195
+ [tree model](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/trees-3d.md)
196
+ instead. Each part takes its colour from `modelColors`, or from the buildings' colour when the
197
+ design gives it none, so the model follows the theme from day to night; vertex colours lighter or
198
+ darker than the rest of their part shade it within that colour.
199
+ - **Metres round an origin on the ground.** The origin is where the model stands, with the model
200
+ above it and its own north along +y once read Z up; nothing may lie more than half a metre below
201
+ it.
202
+ - **What it stands in for.** A mesh whose material is named `footprint` is never drawn: it lays
203
+ out on the ground what the model stands in for, such as a landmark's whole base. Without one, the
204
+ model's own outline on the ground stands in, courtyards included.
205
+ - **Levels of detail.** `<id>.mid.glb` and `<id>.far.glb`, when present, are drawn farther from the
206
+ camera's centre, from about 900 m and 2.6 km.
207
+ - **Budgets.** A level has at most 200,000 triangles and 200,000 vertices, and a map at most 64
208
+ building models. Triangles of the default scene are read with their node transforms; textures,
209
+ animations and compressed geometry (Draco or meshopt) are not.
210
+
211
+ The build reads each model, reports a problem with the file and what to change, works out its
212
+ footprint and writes it beside the style as a prepared model file: a glTF binary whose meshes are
213
+ quantised (`KHR_mesh_quantization`) and compressed by meshoptimizer (`EXT_meshopt_compression`),
214
+ which any glTF viewer opens. The renderer downloads a model only when it stands near the tiles round
215
+ the camera, keeps the nearest dozen, and builds the tiles under it again without the buildings that
216
+ reach more than half a metre into its footprint; a neighbour that only touches it stays. A model
217
+ that cannot be downloaded leaves those buildings standing. Reading a prepared model file loads
218
+ meshoptimizer's WebAssembly decoder, so a page with a Content Security Policy allows
219
+ `'wasm-unsafe-eval'` in its `script-src`.
220
+
221
+ ## Roofs
222
+
223
+ A building whose tiles carry a roof shape (World's `roof_shape`) gets that roof: `gabled`,
224
+ `hipped`, `half_hipped`, `pyramidal`, `skillion`, `gambrel`, `mansard` and `saltbox` roofs, and
225
+ faceted `dome`, `onion` and `round` ones. The ridge follows the building's long side, or
226
+ `roof_orientation` and `roof_direction` when the tiles state them, and the walls rise to the roof,
227
+ so gable ends are walls. Each slope takes the sun, shadows and edges like any face: the ridge and
228
+ the hips are drawn as edges too.
229
+
230
+ The roof's height is `roof_height` where the tiles state it, taken from the building's height.
231
+ Otherwise the roof keeps the pitch of its shape (30° for a gabled or hipped roof, a half sphere for a
232
+ dome); it stands on the walls when the height came from levels or a fallback, and reaches the
233
+ building's height when that was measured (`height_source` `height`). A building without a roof
234
+ shape, a flat roof, a building with courtyards or one far from a rectangle (an L, a U) keeps a flat
235
+ roof, and so does a building a tile's buffer cuts short.
46
236
 
47
237
  ## Light and shadows
48
238
 
49
- The light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
239
+ `threeDimensional.lighting` defaults to `'style'`: it uses MapLibre's `light` position,
240
+ anchor, colour and intensity, together with the authored extrusion's `verticalGradient`.
241
+ Changing the scene sun does not change this base lighting. The style's native lighting defaults
242
+ apply when it declares no light.
243
+
244
+ With `lighting: 'sun'`, the light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
50
245
  application places by direction or by date and place (see
51
246
  [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md)).
52
247
  A sunlit roof keeps the building's colour, a wall turned to a low sun glows, and shade takes the
@@ -59,49 +254,27 @@ any wall, so roofs and walls read apart under the moon.
59
254
  MapLibre's own layers take no shadows, so the renderer darkens the map it is drawn over: after its
60
255
  buildings, it multiplies the ground by the shadows and by a band at the foot of the walls. Labels
61
256
  and icons drawn above it keep their colours. Layers kept under 3D content with
62
- `createTileflowGroundLayers` are darkened with the ground.
63
-
64
- `shadows` and `occlusion` (the band at the foot of the walls and the darker tight corners) default
65
- to true and can be turned off. `windows: true` adds invented night windows, lit by rooms; they are
66
- decoration, not data, and are off by default.
257
+ `createTileflowGroundLayers` are darkened with the ground. Other Core 3D layers on the map, such as
258
+ [3D trees](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/trees-3d.md), share
259
+ the buildings' shadows when `shadows: true`. With it off, buildings neither cast nor receive
260
+ scene shadows, even when another 3D layer uses them.
67
261
 
68
262
  ## Edges
69
263
 
70
264
  Where one building ends and the next starts stays legible at any distance: each roof's outline and
71
- each building's outer corners catch the light in a line one CSS pixel wide, whatever the zoom, and
72
- each building is a little lighter or darker than its colour (up to 6%), so neighbours of one colour
73
- stay apart. The lines are strongest at night; diagonals inside a roof, inner corners and the cuts at
74
- a tile's edge draw none. `edges` sets their strength from 0 (none) to 1, the default.
75
-
76
- `edgeColor` gives the lines a colour of the application's own, as a CSS colour or per theme: a dark
77
- outline as on a printed map, or a neon one at night. It is drawn as it is, unlit, at the full
78
- `edges` strength by day and night. The renderer asks for each theme's colour once and mixes the
79
- colours of the themes either side of a blend's position; a theme without a valid colour keeps the
80
- default.
81
-
82
- <!-- docs:check -->
83
-
84
- ```ts
85
- import {
86
- createTileflowBuildings3D,
87
- type TileflowBuildings3DMap,
88
- type TileflowThemeController,
89
- } from '@tileflow/core/browser';
265
+ each building's outer corners catch the light in a line one CSS pixel wide, whatever the zoom, when enabled. `colorVariation` independently varies building brightness by up to the specified
266
+ fraction (0–1); it defaults to 0, so authored colours remain unchanged. Use 0.06 for the earlier
267
+ 6% variation. The lines are strongest at night; diagonals inside a roof, inner corners and the cuts at
268
+ a tile's edge draw none. `edges` sets their strength from 0 (the default, none) to 1.
90
269
 
91
- export function neonEdges(map: TileflowBuildings3DMap, controller: TileflowThemeController) {
92
- return createTileflowBuildings3D({
93
- map,
94
- controller,
95
- // Cyan outlines at night, a dark ink by day.
96
- edgeColor: (theme) => (theme === 'night' ? '#7fd8ff' : '#2b2a33'),
97
- });
98
- }
99
- ```
270
+ `edgeColor` gives the lines a colour of the map's own, per theme through a token: a dark outline as
271
+ on a printed map, or a neon one at night. It is drawn as it is, unlit, at the full `edges` strength
272
+ by day and night, mixed between the themes either side of a blend's position.
100
273
 
101
274
  ## Night light
102
275
 
103
- At night (as the sun's `brightness` falls), the buildings light the street from data, quietly by
104
- default:
276
+ With `nightLight` above 0, the buildings light the street from data at night (as the sun's
277
+ `brightness` falls):
105
278
 
106
279
  - Landmarks (World's `building_tone` `destination`) and towers of 40 m or more, one in two, are
107
280
  flood-lit at their foot, on the wall and the ground around it.
@@ -112,47 +285,21 @@ default:
112
285
  dark.
113
286
 
114
287
  The night use comes from World's `night_use` on each building. In tiles whose buildings do not
115
- carry it, the renderer finds it in the tile's places (`placesLayer`, `poi` by default): a place
116
- inside a building, or within a few metres of its facade, gives the building its use.
288
+ carry it, the renderer finds it in the tile's places: a place inside a building, or within a few
289
+ metres of its facade, gives the building its use.
117
290
 
118
- `nightLight` sets how bright all of this shines, from 0 (none) to 2; it defaults to 1.
291
+ `nightLight` sets how bright all of this shines, from 0 (none) to 2; it defaults to 0.
119
292
 
120
293
  For the streets themselves, see
121
294
  [street lights](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/street-lights.md),
122
295
  which draws over the buildings so they hide the lamps behind them; both layers share each tile's
123
296
  download.
124
297
 
125
- ## Colours
126
-
127
- Without `color`, buildings take the theme's 3D building colour, the colour Tileflow's buildings
128
- module compiles for its extrusion, and follow theme changes and blends.
129
-
130
- `color(properties, theme)` colours each building from its feature properties for each theme, as a
131
- CSS colour. The renderer calls it once per building and theme when a tile or the set of themes
132
- changes, and mixes the colours of the themes either side of a blend's position on the GPU, so a
133
- moving blend costs nothing per frame:
298
+ ## Options
134
299
 
135
- <!-- docs:check -->
136
-
137
- ```ts
138
- import {createTileflowBuildings3D, type TileflowBuildings3DMap} from '@tileflow/core/browser';
139
-
140
- const tints: Record<string, readonly string[]> = {
141
- day: ['#f1d4cd', '#d3e8da', '#f6e0b5', '#e2d5f0', '#cfe3f2'],
142
- night: ['#353d41', '#2f3a3d', '#3a3b44', '#33363f', '#2d3540'],
143
- };
144
-
145
- export function tintedCity(map: TileflowBuildings3DMap) {
146
- return createTileflowBuildings3D({
147
- map,
148
- color(properties, theme) {
149
- const floors = Math.floor(Number(properties.render_height ?? 5) / 3.2);
150
- const palette = tints[theme] ?? tints.day!;
151
- return palette[floors % palette.length]!;
152
- },
153
- });
154
- }
155
- ```
300
+ The renderer's own options say only how it is delivered: `minzoom` (14.5 by default), `maxTiles`,
301
+ `beforeId` to place the layer before another one, and `transformRequest` to apply the map's request
302
+ rules to the tiles it reads.
156
303
 
157
304
  ## Cost
158
305
 
@@ -164,5 +311,11 @@ way kept the frame pacing of the same city drawn without shadows.
164
311
 
165
312
  - Mercator only; the layer draws nothing below its `minzoom`.
166
313
  - No terrain: buildings stand on flat ground.
167
- - One building colour per feature and theme; facades, roof shapes and building parts as separate
168
- shapes are not drawn.
314
+ - One building colour per feature and theme, on its roof and walls alike; facades are not drawn.
315
+ - Roofs follow the building's smallest enclosing rectangle: a building with courtyards or far from a
316
+ rectangle keeps a flat roof, and `sawtooth` roofs are drawn flat.
317
+ - A colour expression is evaluated at zoom 16.
318
+ - Building models and landmarks take no edges or night light of their own yet, and cannot be
319
+ picked or styled by state.
320
+ - Hosted deploys do not take a map's own model files yet: `tileflow validate --target hosted` reports
321
+ them. Models named by `url` deploy as they are.
@@ -249,8 +249,19 @@ The closed category vocabulary is `arts-entertainment`, `education`, `food-drink
249
249
  `transport`, and `visitor-amenity`. Producer `type` and `icon` values use stable snake_case names.
250
250
  When an `icon` sprite exists it is used directly; an unknown or absent icon safely falls back to the
251
251
  category's themed image role. `labels` and `icons` are booleans. Category styles control only
252
- presentation and may hide or delay candidates, never promote them above the producer's selection.
253
- `placement.coupleIconAndLabel` keeps a POI's icon and label atomic during collision placement.
252
+ presentation and never promote candidates above the producer's selection.
253
+ `placement.coupleIconAndLabel` keeps a POI's icon and label atomic during collision placement:
254
+ both are required.
255
+
256
+ MapLibre orders symbol placement only within a layer, so the module compiles every category into
257
+ one layer per representation and orders it by `filter_rank` and `size_rank`: a station is placed
258
+ before a cash machine whatever their categories. Per-category appearance becomes a `match` on
259
+ `category`; zoom curves are hoisted above it so each category keeps its exact curve. Categories that
260
+ need different layer-wide properties use `placement.order: 'categories'`, one layer per category in
261
+ listed order. `placement.prominentRank` places the most important ranks in a second layer above road
262
+ and place labels, which keeps landmarks visible on maps whose labels give street names priority.
263
+ When icons and labels are both drawn, unnamed POIs are omitted except the self-explanatory types in
264
+ `placement.iconOnlyTypes` (parking and public toilets by default), which draw only their pictogram.
254
265
 
255
266
  ```ts
256
267
  poi({
@@ -263,4 +274,4 @@ poi({
263
274
 
264
275
  The same contract drives regular POI layers, custom HUD treatments, semantic interactions, and
265
276
  feature inspection. There is intentionally no compatibility path for the former class mapping,
266
- rank ceilings, or named density/detail presets.
277
+ rank ceilings, per-category densities, or named density/detail presets.
@@ -295,10 +295,129 @@ opt-in.
295
295
 
296
296
  The vector palette uses thresholds −11 000, −8 000, −6 000, −4 000, −2 000, −1 000, −500, −200,
297
297
  −100, −50, −20, −10, and 0 metres. `bands`, `contours`, and `labels` remain separate controls.
298
- Until native contour fields are contracted, contours are a visual fallback drawn from band polygon
299
- edges, not surveyed isolines. The V1 schema exposes optional source-layer names
300
- `bathymetry_contour`, `seafloor_landform`, `water_name`, and `bathymetry_coverage`, but promises
301
- fields only for required `bathymetry.min_depth` and `bathymetry.sort_key`.
298
+ Without an optional-layer declaration, contours and depth labels retain the legacy polygon-band
299
+ behavior. `marine` shorthands and existing maps keep that behavior.
300
+
301
+ ### Declare contour and name layers
302
+
303
+ For a compatible source, `vectorLayers` explicitly declares optional layers. Core never fetches
304
+ TileJSON to detect them and never substitutes polygon edges for declared contour lines. A missing
305
+ declared layer therefore draws no features; validate the selected dataset before rendering.
306
+ `display: 'bands'` or `'hybrid'` loads the vector source.
307
+
308
+ | Declaration | Source layer | Geometry and fields |
309
+ | --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- |
310
+ | `contours: true` | `bathymetry_contour` | Lines with numeric `depth`, negative metres below the source vertical datum. Zero and positive depths are excluded. |
311
+ | `landforms: true` or a range inventory | `seafloor_landform` | Named points, lines, or polygons with stable source `id`, provider `class`, and `name`. |
312
+ | `waterNames: true` or a range inventory | `water_name` | Named points, lines, or polygons with the same name contract. |
313
+
314
+ Name geometry stays in its source representation. `placement: 'point'` (the default) lets the
315
+ renderer place names inside polygons or on points. Use `placement: 'line'` to label source lines.
316
+ Core creates no substitute point, centroid, or line feature.
317
+
318
+ Name layers preserve source properties; `id` is a source identifier, with no guarantee of a numeric
319
+ renderer feature ID. Optional `name:<language>` fields retain the source's language assignments.
320
+ `labels({language: 'es'})` selects a nonempty `name:es`, falling back to `name`; `auto` and `local`
321
+ use `name` directly. Core does not infer the language of an unqualified name or invent translations.
322
+ Optional `min_zoom` is inclusive and `max_zoom` exclusive, defaulting to 0 and 24. These nominal
323
+ ranges intersect the symbol style's range. With `true`, MapLibre evaluates feature zoom filters at
324
+ integer zooms: `min_zoom: 7.6` first becomes eligible at zoom 8. For exact fractional boundaries,
325
+ declare an exhaustive range inventory instead:
326
+
327
+ ```ts
328
+ vectorLayers: {
329
+ contours: true,
330
+ landforms: {zoomRanges: [{minZoom: 0, maxZoom: 24}]},
331
+ waterNames: {
332
+ zoomRanges: [{minZoom: 1, maxZoom: 6.5}, {minZoom: 7.6, maxZoom: 10}],
333
+ },
334
+ }
335
+ ```
336
+
337
+ Each declared pair becomes a separate symbol layer with fractional renderer bounds and equality
338
+ filters on the source's original range. A `7.6–10` feature appears at 7.6 and disappears at 10.
339
+ Unlisted pairs are omitted. Core never discovers ranges from tiles: validate the source inventory
340
+ against this exhaustive declaration before rendering, and retain that inventory with reproducible
341
+ captures. Numeric pairs must match the source's encoded values exactly.
342
+
343
+ Each name layer accepts 1–128 unique pairs with `0 <= minZoom < maxZoom <= 24`;
344
+ `tileflowBathymetryNameLayerLimits.maxZoomRanges` exposes the limit. Input order does not affect
345
+ output. Different pairs can overlap in zoom because equality filters keep their feature sets
346
+ disjoint. A style may narrow a pair's visibility; an empty intersection emits no layer.
347
+ Missing or null source bounds default to 0/24. Nonnumeric values use the same numeric-conversion
348
+ fallbacks; numeric strings are converted. Such features draw only if their normalized pair is
349
+ declared. Core preserves the original source properties.
350
+
351
+ Optional numeric `sort_key` defaults to 0; smaller values enter collision placement first within
352
+ each cohort. Cohorts follow ascending minimum then maximum zoom and sit below ordinary map labels.
353
+ Names use normal collision placement without overlap by default.
354
+
355
+ This example declares a source whose regional detail supports zoom 14. Its extended ranges are
356
+ explicit design choices, not a promise of worldwide detail:
357
+
358
+ <!-- docs:check -->
359
+
360
+ ```ts
361
+ import {bathymetry, defineMap, expr, field, fixed, labels, water} from '@tileflow/core';
362
+ import {soundings} from '@tileflow/maps';
363
+
364
+ export default defineMap({
365
+ id: 'regional-depth',
366
+ version: 1,
367
+ extends: soundings,
368
+ marine: {
369
+ bathymetry: bathymetry({
370
+ url: 'https://tiles.example.test/regional-depth/tiles.json',
371
+ vectorLayers: {
372
+ contours: true,
373
+ landforms: {zoomRanges: [{minZoom: 0, maxZoom: 24}]},
374
+ waterNames: {zoomRanges: [{minZoom: 7.6, maxZoom: 10}]},
375
+ },
376
+ contours: {},
377
+ labels: {},
378
+ }),
379
+ nautical: false,
380
+ },
381
+ modules: {
382
+ labels: labels({language: 'es'}),
383
+ water: water({
384
+ bathymetry: {
385
+ maxZoom: 15,
386
+ opacity: fixed(0.8, {reason: 'The selected regional source supplies detailed bands'}),
387
+ },
388
+ bathymetryContours: {maxZoom: 15},
389
+ bathymetryLabels: {
390
+ maxZoom: 15,
391
+ text: {
392
+ field: expr.concat(
393
+ expr.toString(expr.abs(expr.get(field('bathymetryContourDepth')))),
394
+ ' m',
395
+ ),
396
+ },
397
+ },
398
+ bathymetryLandforms: {maxZoom: 15},
399
+ bathymetryWaterNames: {maxZoom: 15},
400
+ }),
401
+ },
402
+ });
403
+ ```
404
+
405
+ `water.bathymetryContours` and `water.bathymetryLabels` now consume the declared lines and `depth`;
406
+ labels default to line placement. Band fills still consume `min_depth` and `sort_key`.
407
+ `water.bathymetryLandforms` and `water.bathymetryWaterNames` style the declared names; set
408
+ `visible: false` to hide either. Without their declaration, those styles emit no name layers.
409
+ Name styles may use `class` in expressions without changing the source classification.
410
+
411
+ Water-module styles override product-level `bands`, `contours`, and `labels`. Existing custom
412
+ `text.field` expressions remain authoritative: when refining a map from legacy polygons to native
413
+ contours, use `expr.get(field('bathymetryContourDepth'))` for its depth. That auxiliary binding
414
+ resolves to `depth` independently of the primary World schema; `bathymetryMinDepth` keeps its
415
+ legacy band meaning. Raw `expr.get('depth')` is rejected by authoring validation.
416
+ Increasing only `maxZoom` does not remove an opacity curve that fades out earlier. Extend ranges
417
+ and opacity only where the selected source supports the intended detail.
418
+
419
+ The V1 schema also reserves `bathymetry_coverage`; Core adds no coverage rendering by default.
420
+ Derived contours and names provide geographic context, not certified navigation data.
302
421
 
303
422
  The DEM enables future client-side sampling and depth profiles, but Core intentionally exposes no
304
423
  profile or remote sampling API yet. Rendered relief must not be interpreted as navigation-safe
@@ -318,6 +437,15 @@ explicit because the data contract cannot assume a sprite name. They require pos
318
437
  `markings` evidence and reject `crossing=no`; both fields are semantic bindings, so custom schemas
319
438
  can remap them without weakening that rule.
320
439
 
440
+ Metric road rings scale continuously with the map from zoom 15 through 24, including fractional
441
+ zooms. Their casing is below surface road fills so approaching streets cover the outline at each
442
+ junction. The ring fill stays below bridges and road markings.
443
+
444
+ Road treatment `widthScale` scales both the stroke and an authored `gapWidth`, keeping hollow
445
+ casings aligned with the carriageway at fractional zoom levels.
446
+ When every dash treatment is continuous (`[1, 0]`), road layers omit the dash pattern so thin
447
+ casings render as solid strokes. Actual construction and tunnel dashes remain patterned.
448
+
321
449
  Datasets may also publish the optional, decorative `canopy` layer: points inside urban parks and
322
450
  gardens, at most one per cell of a grid of about 20 m that measured tree canopy covers. Each point
323
451
  tells how much room a drawn grove of tree crowns has and how dense the canopy around it is. The
@@ -421,7 +549,11 @@ buildings. Its `activity_score`, `rank`, `min_zoom`, and `confidence` fields con
421
549
  warm tint without drawing POI-radius circles. Current building footprints may carry sparse
422
550
  `building_tone=commercial|destination|active`; absence means the neutral base building. The three
423
551
  values preserve why the warm tone was selected even when a theme maps them to the same color.
424
- Building visibility is fixed by the layer zoom contract and never by optional semantics. The land
552
+ Buildings may also carry World's `class`, their use in Overture's building subtype values; without
553
+ a tone, the buildings module colours `residential`, `commercial`, public buildings (`civic`,
554
+ `education`, `medical`, `religious` and the like) and works (`industrial`, `agricultural`,
555
+ `service`) with the theme's `residential`, `commercial`, `civic` and `industrial` building colours,
556
+ and every other building with `generic`. Building visibility is fixed by the layer zoom contract and never by optional semantics. The land
425
557
  module also exposes
426
558
  `medical`, `education`, and `government` independently instead of collapsing their already distinct
427
559
  source classes into one civic color.
@@ -39,6 +39,11 @@ preparing the referenced resources.
39
39
 
40
40
  The exported `tileflowNativeProfile` targets MapLibre React Native 11.3.10 with Native Android 13.2.0
41
41
  and iOS 6.26.0. These are static compatibility targets, not a supported application dependency matrix.
42
+ The application integration uses MapLibre React Native 11.5.0 with Android 13.6.1 and iOS 6.31.0.
43
+ The artifact profile retains its earlier validation targets and build-record engine values, keeping
44
+ prepared artifacts compatible with existing Hosted validators. New application dependencies do not
45
+ expand the accepted style vocabulary.
46
+
42
47
  The wrapper's Style Spec version is 26.2.1. Tileflow retains its existing compiler and parser at
43
48
  24.8.5: `native-v1` checks that compiler's vocabulary and the intersection of its recorded Android
44
49
  and iOS support. It does not expose every feature of Style Spec 26.2.1 or upgrade the web compiler.
@@ -121,7 +121,7 @@ network request. Do not attach a credential merely because this helper accepted
121
121
 
122
122
  ## Private parser and source builds
123
123
 
124
- The native entry embeds the low-level parser from `whatwg-url` 15.1.0, `tr46` 6.0.0 (Unicode 17
124
+ The native entry embeds the low-level parser from `whatwg-url` 17.2.0, `tr46` 7.0.0 (Unicode 18
125
125
  IDNA processing) and Punycode.js 2.3.1. It exposes no URL class or parser configuration. Package
126
126
  consumers only install Core; the published `dist/native.js` has no external runtime imports.
127
127
  The parser is a development/build dependency, not an additional runtime package to install.
@@ -136,8 +136,10 @@ policy with its existing host parser; it does not embed the mobile parser in its
136
136
 
137
137
  From an SDK source checkout, run the ordinary Core build, not a direct source import in a native
138
138
  application. `tsup.native.config.ts` builds only the native entry with `platform: 'browser'`,
139
- `target: 'es2020'`, ESM and no splitting. Its plugin redirects exactly the two relative
140
- `./encoding` imports in the pinned parser modules to Core's private UTF-8 codec. It rejects an
139
+ `target: 'es2020'`, ESM and no splitting. Its plugin redirects the two relative
140
+ `./encoding` imports and the parser's two `@exodus/bytes` encoding imports to Core's private UTF-8
141
+ codec. Query encoding accepts only UTF-8; the legacy encoding bootstrap has no side effects.
142
+ Exodus and its legacy encoding tables are not embedded. The build rejects an
141
143
  unexpected dependency version, an unbound codec import or external runtime imports. It does not
142
144
  patch packages on disk, configure a global alias or change another entry's build.
143
145