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

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,19 +1,22 @@
1
1
  # 3D buildings
2
2
 
3
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.
4
+ By default it preserves the map's building colours, uses the scene sun, casts shadows, adds soft
5
+ contact shading and rounds exterior corners by 0.8 metres. An unspecified sun uses soft daylight
6
+ with directional intensity 0.2 and ambient intensity 0.8; authored theme lighting stays in control.
7
+ Showing, hiding and changing height are immediate. Illuminated outlines, random colour variation,
8
+ invented windows and night lights remain off. Every effect is independently overridable and
9
+ independent of the map's trees.
10
10
 
11
11
  Tileflow landmarks and a map's own building models can stand in the same 3D scene, replacing
12
12
  the buildings under their footprints.
13
13
 
14
14
  It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
15
15
  labels. It needs WebGL2 and the Mercator projection, which MapLibre's globe turns into from about
16
- zoom 12; the buildings stand from zoom 14.5 by default.
16
+ zoom 12. On every map, buildings and placed models appear automatically from zoom 15,
17
+ with opacity rising linearly from 0 at 15 to 1 at 15.25. Zooming back out reverses the fade.
18
+ Their height stays unchanged throughout; shadows and contact shading fade with them.
19
+ `hide()` keeps them hidden at every zoom until `show()` is called.
17
20
 
18
21
  ## Show the buildings
19
22
 
@@ -31,15 +34,14 @@ import {
31
34
 
32
35
  export function raiseTheCity(map: TileflowBuildings3DMap, controller: TileflowThemeController) {
33
36
  const buildings = createTileflowBuildings3D({map, controller});
34
- // No animation unless requested.
35
- void buildings.show();
37
+ // Enabled on creation; hide() and show() override visibility.
36
38
  return buildings;
37
39
  }
38
40
  ```
39
41
 
40
42
  `show()`, `hide()` and `grow(scale)` change the height immediately by default (duration 0).
41
43
  `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.
44
+ smooth opacity fade. The default animation mode is `grow`; both modes respect reduced-motion settings.
43
45
  To animate, pass `{transition: {duration: 800}}` explicitly; `grow(scale)` accepts a height share
44
46
  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
45
47
  motion changes at once. `dispose()` takes the layer off the map.
@@ -51,7 +53,7 @@ tiles (25 by default), and meshes them a few milliseconds per frame. It reads Wo
51
53
  `night_use` and the roof fields (see [roofs](#roofs)), and the places in World's `poi` layer (see
52
54
  [night light](#night-light)). `stats.declared` tells whether the map's
53
55
  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.
56
+ compiled metadata keeps explicit settings, including `false`, `0` and `lighting: 'style'`.
55
57
 
56
58
  ## Design the buildings in the map
57
59
 
@@ -108,13 +110,36 @@ export default defineMap({
108
110
  set of themes changes, and the themes either side of a blend's position mix on the GPU, so a
109
111
  moving blend costs nothing per frame.
110
112
  - `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
113
+ corners) default to true. Set either to false independently. `windows: true` adds invented night windows, lit by rooms; they are
112
114
  decoration, not data, and are off by default. Where World says what a building is used for
113
115
  (`class`), sheds, garages, barns, factories, churches and service buildings get no rows of
114
116
  windows, homes light more of theirs at night, in a warm light, and offices, schools and hospitals
115
117
  fewer, in a cooler one.
116
118
  - `threeDimensional.edges` and `edgeColor` set the [edges](#edges), and `nightLight` the
117
119
  [night light](#night-light). Numbers and colours may be theme tokens.
120
+ - `threeDimensional.edgeRadius` rounds convex exterior corners in the mesh and softens the
121
+ roof/wall lighting transition. It accepts 0–1 metres and defaults to 0.8; use 0 for sharp corners.
122
+ Courtyard corners and tile cuts stay unchanged. The authored roof shape is preserved; the top
123
+ edge is softened through lighting, not a geometric roof bevel. Rounding adds a bounded number
124
+ of vertices at convex corners, with no extra render pass. Changes to this radius rebuild the
125
+ resident building meshes.
126
+
127
+ In both `style` and `sun` lighting, the vertical gradient darkens the foot of a facade by ten percent, fading smoothly over
128
+ at most three metres (half the height for low buildings). It is evaluated per fragment, so a tall
129
+ building does not acquire a full-height dark ramp. Disabling the authored extrusion's
130
+ `verticalGradient` disables this shading. Ground contact shadow is controlled by `occlusion`, enabled by default. It extends three metres
131
+ from grounded walls, darkening the ground by at most 16%, with a smooth distance falloff and round
132
+ corners. Every map uses these defaults when it enables `occlusion`. The reach is measured in world
133
+ metres, so its screen width scales continuously with zoom and becomes subtle at distant views.
134
+ Overlaps use the strongest
135
+ shade once, so shared walls, courtyards and tile joins do not accumulate dark bands. Raised building
136
+ parts do not cast contact shade. Facade occlusion darkens the base by at most 14% and fades
137
+ exponentially with a three-metre decay radius: about 5% remains one floor up and 2% two floors up.
138
+ It follows height above ground, so raised parts do not restart the contact gradient at their base.
139
+ The facade gradient, contact and inner-corner shade use the strongest contribution once, avoiding
140
+ an extra dark strip where they overlap. Both lighting modes use the same falloff at every zoom.
141
+ The ground mask uses one byte per drawing-buffer pixel while enabled and is released when disabled
142
+ or the renderer is removed; it resizes with the drawing buffer.
118
143
 
119
144
  ## Landmarks
120
145
 
@@ -236,16 +261,49 @@ roof, and so does a building a tile's buffer cuts short.
236
261
 
237
262
  ## Light and shadows
238
263
 
239
- `threeDimensional.lighting` defaults to `'style'`: it uses MapLibre's `light` position,
264
+ Sun lighting and cast shadows are SDK defaults, including for Streets. Each map can choose its
265
+ own finish. For native extrusion lighting, sharp corners and no shadows in an extending map:
266
+
267
+ <!-- docs:check -->
268
+
269
+ ```ts
270
+ import {defineMap, refine} from '@tileflow/core';
271
+ import {streets} from '@tileflow/maps';
272
+
273
+ export default defineMap({
274
+ id: 'graphic-streets',
275
+ name: 'Graphic streets',
276
+ version: 1,
277
+ extends: streets,
278
+ modules: {
279
+ buildings: refine({
280
+ threeDimensional: {lighting: 'style', edgeRadius: 0, shadows: false, occlusion: false},
281
+ }),
282
+ },
283
+ });
284
+ ```
285
+
286
+ Select `timeOfDay` through your map adapter to change the authored direction, colour and strength
287
+ of direct and ambient light. Streets includes moonlight and shadows in its night presets. These
288
+ presets are independent of the user's clock and location. In DevTools, use the 3D view and
289
+ **Map → Buildings → Building light / Cast shadows**. The theme's `lighting.sun.shadowIntensity`
290
+ controls shadow strength; `lighting.ambient` fills the shade. Use `controller.setSun({date,
291
+ location})` for an astronomical direction instead, as described in
292
+ [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md).
293
+
294
+ `threeDimensional.lighting: 'style'` uses MapLibre's `light` position,
240
295
  anchor, colour and intensity, together with the authored extrusion's `verticalGradient`.
241
296
  Changing the scene sun does not change this base lighting. The style's native lighting defaults
242
297
  apply when it declares no light.
243
298
 
244
- With `lighting: 'sun'`, the light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
299
+ With the default `lighting: 'sun'`, the light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
245
300
  application places by direction or by date and place (see
246
301
  [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md)).
302
+ Without a controller, the renderer reads the compiled style's sun, falling back to the style's
303
+ light direction and then the SDK daylight. It refreshes this cached light on style changes.
247
304
  A sunlit roof keeps the building's colour, a wall turned to a low sun glows, and shade takes the
248
- sky's colour. As the sun sets, shadows lengthen while they fade; a sun below the horizon casts none.
305
+ sky's colour. Shadows lengthen as the sun lowers, with their projected direction limited to 15°
306
+ above the horizon. The actual light retains its authored altitude; a sun below the horizon casts none.
249
307
 
250
308
  A wall sees half the sky, so at night, when the ground gives back little light, walls in shade
251
309
  stay darker than roofs. A theme's night `lighting.sun` set high (60° or more) lights roofs more than
@@ -297,15 +355,37 @@ download.
297
355
 
298
356
  ## Options
299
357
 
300
- The renderer's own options say only how it is delivered: `minzoom` (14.5 by default), `maxTiles`,
358
+ The renderer's own options say only how it is delivered: `minzoom` (15 by default, with a 0.25-zoom opacity ramp), `maxTiles`,
301
359
  `beforeId` to place the layer before another one, and `transformRequest` to apply the map's request
302
360
  rules to the tiles it reads.
303
361
 
304
362
  ## Cost
305
363
 
306
- The shadow map (up to 4096 × 4096) is drawn again only when the sun, the buildings or their height
307
- change, not when the camera moves. In a campaign over Miami at 1512 × 860 at 2×, a city lit this
308
- way kept the frame pacing of the same city drawn without shadows.
364
+ During partial opacity, a depth-only geometry pass resolves the nearest surfaces before colour
365
+ blending, preventing rear walls from bleeding through. At full opacity only the normal geometry
366
+ pass runs; zoom fading schedules no extra animation frames or per-zoom mesh rebuilds.
367
+
368
+ The browser bundle prepares building meshes in one worker per renderer and transfers the finished
369
+ vertex and index buffers. No worker URL or extra asset hosting is needed. A Content Security Policy
370
+ must allow `worker-src blob:` to use this path; if workers are unavailable or blocked, the renderer
371
+ falls back to local meshing. The worker is terminated on `dispose()`. Pending tile updates retain
372
+ the previous mesh until their replacement is ready; cancelled and obsolete results are discarded.
373
+
374
+ Mercator rendering batches buildings spatially and skips batches outside the camera. Shadow
375
+ casting uses the light's bounds independently, so an offscreen building can still cast a visible
376
+ shadow. Ground contact and flood bands retain their physical reach when testing visibility.
377
+ Rounded corners use fewer segments when their screen radius is small and retain the original
378
+ tessellation up close. Latitude, radius and pitch inform this choice; very oblique views keep full
379
+ detail. Changes are prepared asynchronously; heights, feature colours,
380
+ and the contact-shadow settings stay the same. GPU uploads remain limited to two tiles per frame.
381
+
382
+ The shared shadow map is at most 4096 × 4096. On a planar view it concentrates resolution on visible
383
+ ground and raised surfaces, while every loaded caster can still project into that area. A snapped
384
+ Mercator texel grid stays stable during small pans, including changes of latitude, without fixing
385
+ the meshes' physical metre scale. One filtered depth comparison keeps edges sharp.
386
+ Missing camera capabilities, terrain or a view near the horizon use the full loaded area instead.
387
+ The depth pass runs again when its focused area, light or casting geometry changes and is reused
388
+ when they stay unchanged.
309
389
 
310
390
  ## Limits
311
391
 
@@ -317,5 +397,6 @@ way kept the frame pacing of the same city drawn without shadows.
317
397
  - A colour expression is evaluated at zoom 16.
318
398
  - Building models and landmarks take no edges or night light of their own yet, and cannot be
319
399
  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.
400
+ - A hosted map takes up to 64 building models of its own, 32 MiB once prepared; `tileflow deploy`
401
+ uploads them with the map. A deploy with `--with-native` does not take them, since MapLibre Native
402
+ draws no models. Models named by `url` deploy as they are.
@@ -0,0 +1,142 @@
1
+ # Cartographic defaults
2
+
3
+ Start with the [Core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md)
4
+ for installation and a complete map. Semantic modules supply a coordinated starting appearance;
5
+ module styles and owner-local render targets can replace those decisions.
6
+
7
+ ## Roads and roundabouts
8
+
9
+ `roads({rampOrder: 'below-roads'})` draws ramp fills before receiving carriageways in the same
10
+ surface, bridge or tunnel band. This prevents a rounded ramp endpoint covering the receiving
11
+ road. Choose `'by-class'` to retain class-based fill order instead. Explicit render-stack moves
12
+ remain available; ramp fills have targets `roads.classes.<class>.<structure>.rampFill` when the
13
+ separate ramp order is active. A selector refinement replaces that target's complete filter, so
14
+ retain its ramp eligibility when writing a custom selector. Module class fill styles apply to
15
+ both passes; a refinement of a specific `fill` or `rampFill` target changes only that pass.
16
+
17
+ Core owns circular-road geometry. Physical inner and outer radii take precedence when provided
18
+ by the source. Otherwise the ring follows the enabled class's effective surface width, including
19
+ width treatments. The default border follows the actual casing width and gap. Hidden classes and
20
+ surface fills do not leave orphan rings. Ring colors remain semantic palette choices, independently
21
+ replaceable through `roundabouts.fill.strokeColor` and `roundabouts.casing.strokeColor`; they do
22
+ not copy arbitrary per-class color curves.
23
+
24
+ Metric scaling and base-2 width curves retain their native interpolation. Other width curves are
25
+ sampled into the shared geometry curve: numeric widths target 0.1-pixel error, stopping refinement
26
+ once intervals reach 1/32 zoom; very large widths can exceed that target. Feature-dependent width
27
+ expressions retain authored and integer zoom stops without adaptive refinement, keeping expression
28
+ size bounded. Their interpolation between stops is approximate, with no universal pixel-error bound.
29
+ Step changes complete at the authored zoom, with the transition confined to the preceding 0.000001
30
+ zoom interval. Explicit circle radius and stroke-width expressions bypass this sampling when exact
31
+ independent behavior is required.
32
+
33
+ `roundabouts.fallbackWidth` replaces only the fallback carriageway width in pixels;
34
+ `roundabouts.borderWidth` replaces the total extra width around the ring. Existing circle styles
35
+ can independently replace radius, stroke width, color, opacity and zoom range. For example, this
36
+ module fragment retains geometric defaults but gives rings a deliberate fixed border:
37
+
38
+ <!-- docs:check -->
39
+
40
+ ```ts
41
+ import {roads} from '@tileflow/core';
42
+
43
+ export const customRoads = roads({
44
+ rampOrder: 'by-class',
45
+ roundabouts: {borderWidth: 0},
46
+ });
47
+ ```
48
+
49
+ ## Ground surfaces
50
+
51
+ The land module draws broad cover and park surfaces before mapped grass, meadow, scrub and
52
+ flowerbed details. Parking, sports pitches, tracks, playgrounds and zoos follow those details so
53
+ they remain visible inside broad green polygons. Both Tileflow World and OpenMapTiles' combined
54
+ park source follow this order. These defaults do not change the map's colors, opacity or textures.
55
+
56
+ A thematic map can give a park tint more prominence through the existing render stack. This
57
+ module fragment is ready to use as `modules.land` in a map:
58
+
59
+ <!-- docs:check -->
60
+
61
+ ```ts
62
+ import {land, refineRenderTarget, withRenderStack} from '@tileflow/core';
63
+
64
+ export const thematicLand = withRenderStack(land(), {
65
+ parkTint: refineRenderTarget({
66
+ target: 'land.landcover.urbanPark.fill',
67
+ placement: 'above-land',
68
+ }),
69
+ });
70
+ ```
71
+
72
+ Refinements target contributions that the map actually draws. Disabling a target and retaining
73
+ a refinement for it is an authoring error. Disabling the whole module removes its contributions
74
+ and its render stack. The built-in surface order needs no target anchors and works when individual
75
+ surfaces are hidden.
76
+
77
+ ## Street names on the ground or in the foreground
78
+
79
+ `labels({roadPlacement: 'foreground'})` is the default. Names draw above buildings and vegetation.
80
+ Use `roadPlacement: 'ground'` to draw them below buildings, with text aligned to the map surface.
81
+ Shields and junction numbers remain in the foreground. Ground placement does not require the
82
+ buildings module: street names still draw when buildings are disabled.
83
+
84
+ This module fragment keeps the normal language selection and road-class filters while opting
85
+ into ground names. Individual symbol styles can override the default text alignment:
86
+
87
+ <!-- docs:check -->
88
+
89
+ ```ts
90
+ import {labels} from '@tileflow/core';
91
+
92
+ export const groundLabels = labels({
93
+ roadPlacement: 'ground',
94
+ collisionPriority: 'navigation',
95
+ language: 'local',
96
+ roads: 'all',
97
+ styles: {
98
+ roads: {
99
+ primary: {text: {pitchAlignment: 'viewport'}},
100
+ },
101
+ },
102
+ });
103
+ ```
104
+
105
+ MapLibre offers foreground symbols first when resolving collisions. Ground names therefore yield
106
+ to POIs and other foreground symbols, even with `collisionPriority: 'navigation'`. Navigation
107
+ priority still orders the ground road classes relative to one another; it does not make their
108
+ collision priority independent of drawing order. Foreground placement retains the existing
109
+ navigation priority over ordinary POIs. Neither mode turns collision detection off.
110
+
111
+ Use a refinement on `labels.roads.<class>` to move an individual class after this default has been
112
+ applied. Symbol size, zoom range, spacing, priority, fonts and halos remain available in
113
+ `styles.roads`. The same names, language fallbacks and class selectors are used in both modes.
114
+
115
+ ## Discover and override decisions
116
+
117
+ `tileflow describe labels --json` exposes the installed options, and `tileflow inspect --json`
118
+ shows resolved inheritance and merge provenance. `tileflow explain --inspection --json` adds
119
+ compiled semantic targets and physical provenance. These commands describe the authored map;
120
+ they do not rewrite explicit style choices.
121
+
122
+ `tileflow describe themes --json` lists the twelve required base color roles and composition
123
+ rules. A [single independent theme](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/themes-and-module-styles.md)
124
+ needs no time-of-day catalog. Optional color roles derive from that theme and accept explicit
125
+ replacements. Removing inherited lighting uses `reset()` inside `defineTheme`; it restores the
126
+ SDK fallback, while building shadows and occlusion have separate on/off controls.
127
+
128
+ `createStyleResult()` and `tileflow inspect compiled --json` include `report.decisions`: actual
129
+ color roles and selected road, label and 3D-building policies, each with its value, source,
130
+ reason and override guidance. Query `--path /report/decisions --limit 80` to bound this output.
131
+ `default` means an SDK fallback, `map` a value in the resolved map, `theme` a role defined by the
132
+ selected theme and `derived` an optional role computed by Core. Use config inspection's merge
133
+ provenance to distinguish inherited map values from leaf overrides. These decisions describe
134
+ module policies and the theme's color context; later module styles and render-stack refinements
135
+ can override them. They are not a trace of every final paint property or a visual-quality score.
136
+ Simple `createStyle()` and browser drawing do not collect this report.
137
+
138
+ Replace an inherited module with its constructor, patch it with `refine()`, restore an inherited
139
+ value with `reset()`, or remove the whole domain with `disable()`. See the
140
+ [authoring model](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/authoring-model.md)
141
+ for these composition rules. Capture contrasting designs and compare them at matching cameras to
142
+ check both the default result and intentional departures from it.
@@ -121,14 +121,14 @@ their source faces' shapes. Browser adapters load local faces before MapLibre. N
121
121
  `glyphs` URL provider instead, which enumerates the exact comma-joined MapLibre request keys in
122
122
  `fontStacks`.
123
123
 
124
- Streets, Ferraris, Härad, Soundings, and Blueprint each declare
124
+ Streets, Soundings, and Blueprint each declare
125
125
  `https://api.tileflow.dev/base/33d4de5e8086d9d629d67d3f39fedb87e23686c4c1ac653c27e2a52aee9d00b3/glyphs/{fontstack}/{range}.pbf`
126
126
  with the exact `Noto Sans Regular` and `Noto Sans Bold` stacks. The full hash identifies one
127
127
  published, immutable glyph collection independent of World data. The older `/fonts/...` endpoint
128
128
  remains a mutable compatibility URL and does not make a resolved map byte-reproducible. The global
129
129
  glyph `assetSetSha256` is not the same-domain value as the per-map `assetSetSha256` in
130
130
  `build-manifest.json`.
131
- Baedeker and Siegfried each own a packaged directory containing Cormorant Garamond Regular, SemiBold, and Italic.
131
+ Baedeker, 1777, and Siegfried each own a packaged directory containing Cormorant Garamond Regular, SemiBold, and Italic.
132
132
 
133
133
  Name the official generation deliberately, or use another OpenMapTiles-compatible vector source:
134
134
 
@@ -408,6 +408,58 @@ labels default to line placement. Band fills still consume `min_depth` and `sort
408
408
  `visible: false` to hide either. Without their declaration, those styles emit no name layers.
409
409
  Name styles may use `class` in expressions without changing the source classification.
410
410
 
411
+ Mixed geometry can opt into `placementByGeometry` on either name style. Its resolved policy must
412
+ include `point`, `line`, and `polygon`; `point` must be `'point'`, while the other two accept
413
+ `'point'`, `'line'`, or `'line-center'`. The policy overrides scalar `placement`, including an
414
+ inherited value. Without it, scalar placement and existing generated layers remain unchanged.
415
+
416
+ For example, this map labels point features at their coordinates and retains line placement for
417
+ lines and polygon boundaries:
418
+
419
+ <!-- docs:check -->
420
+
421
+ ```ts
422
+ import {bathymetry, defineMap, labels, water} from '@tileflow/core';
423
+ import {streets} from '@tileflow/maps';
424
+
425
+ export default defineMap({
426
+ id: 'marine-names',
427
+ version: 1,
428
+ extends: streets,
429
+ marine: {
430
+ bathymetry: bathymetry({
431
+ vectorLayers: {landforms: {zoomRanges: [{minZoom: 0, maxZoom: 24}]}},
432
+ }),
433
+ nautical: false,
434
+ },
435
+ modules: {
436
+ labels: labels({language: 'es'}),
437
+ water: water({
438
+ bathymetryLandforms: {
439
+ placementByGeometry: {point: 'point', line: 'line', polygon: 'line'},
440
+ },
441
+ }),
442
+ },
443
+ });
444
+ ```
445
+
446
+ This example declares source availability; the selected source must actually contain those
447
+ features. Equal placements share one geometry-filtered group per source range. A policy emits
448
+ at most three groups per range (384 layers for one name family at the 128-range limit). Groups
449
+ compile in `point`, `line`, `line-center` order below ordinary map labels; source priority orders
450
+ features within each group. Module refinements inherit the policy and style every group.
451
+ Render-stack refinements still address exact semantic targets; inspect each placement/range
452
+ target when applying a narrower refinement.
453
+ Normal collision still applies and can hide a name.
454
+
455
+ Placement uses the received geometry: `point` places a line label at its first rendered vertex;
456
+ it does not compute a stable line midpoint. Polygon `point` placement uses an interior position
457
+ of the rendered polygon, while `line` follows its boundary. Core does not generate source anchors,
458
+ merge identities or deduplicate names by text. A source can supply multiple stable point anchors
459
+ with shared identity properties and distinct anchor properties; their extent, visibility and
460
+ editorial policy belong to that source. Keep original source identities as properties rather than
461
+ assuming a universal numeric `feature.id`.
462
+
411
463
  Water-module styles override product-level `bands`, `contours`, and `labels`. Existing custom
412
464
  `text.field` expressions remain authoritative: when refining a map from legacy polygons to native
413
465
  contours, use `expr.get(field('bathymetryContourDepth'))` for its depth. That auxiliary binding
@@ -53,10 +53,8 @@ import {
53
53
  civica,
54
54
  civicaFonts,
55
55
  civicaIcons,
56
- ferraris,
57
- ferrarisIcons,
58
- harad,
59
- haradIcons,
56
+ map1777,
57
+ map1777Icons,
60
58
  siegfried,
61
59
  siegfriedFonts,
62
60
  siegfriedIcons,
@@ -92,9 +92,16 @@ then transition to Mercator between zoom 10 and 12 so detailed streets remain pl
92
92
  property, or set it to `'mercator'`, for a consistently flat map.
93
93
 
94
94
  Terrain keeps the original `'none' | 'hillshade' | '3d'` shorthand. The object form can style every
95
- MapLibre hillshade paint control, or generate vector contours in the browser from an explicit DEM
96
- tile template. Contours require `demUrl`, `demMaxZoom`, and zoom-indexed `[minor, index]`
97
- `thresholds`; the raster `url` remains the separate TileJSON endpoint. Set `mode: 'none'` to compile
95
+ MapLibre hillshade paint control. Raster terrain accepts either a TileJSON `url` or explicit `tiles`
96
+ templates, with optional `tileSize` (256 or 512) and native `minZoom`/`maxZoom` limits. The templates
97
+ must contain `{z}`, `{x}` and `{y}` and use HTTPS (HTTP is allowed for loopback development).
98
+ These source limits control DEM requests independently of `hillshade.minZoom`/`maxZoom` visibility;
99
+ the renderer reuses the highest available native resolution for closer views. Select resolution
100
+ limits supported by the provider and retain its required attribution.
101
+
102
+ Contours generate vector lines in the browser from their own explicit DEM tile template. They
103
+ require `demUrl`, `demMaxZoom`, and zoom-indexed `[minor, index]` `thresholds`; these are separate
104
+ from the raster source's `url` or `tiles`. Set `mode: 'none'` to compile
98
105
  contours without adding a raster source, hillshade, or 3D terrain:
99
106
 
100
107
  ```ts
@@ -6,6 +6,9 @@ glowing head. Streets and pavements the data maps as lit, but whose lamps it doe
6
6
  soft light along them that claims no lamp's place. As the theme controller's sun darkens the map,
7
7
  the lights switch on one by one; they go out as day comes.
8
8
 
9
+ Street lights are off by default in the SDK and in Streets. A compiled map must explicitly set
10
+ `roads.streetLights.enabled: true` before the renderer or preview will show lamp glows.
11
+
9
12
  It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
10
13
  labels. When `createTileflowBuildings3D` raises the buildings, they hide the lamps behind them. It
11
14
  needs WebGL2 and the Mercator projection, and shines from zoom 14 by default.
@@ -13,7 +16,8 @@ needs WebGL2 and the Mercator projection, and shines from zoom 14 by default.
13
16
  ## Light the streets
14
17
 
15
18
  This example assumes an existing MapLibre map showing a Tileflow map whose vector source is called
16
- `tileflow`, as Tileflow's maps call World, and a theme controller for it:
19
+ `tileflow`, as Tileflow's maps call World, with `roads.streetLights.enabled: true` in its map
20
+ definition and a theme controller for it:
17
21
 
18
22
  <!-- docs:check -->
19
23
 
@@ -56,12 +60,12 @@ lit streets, others almost none. Where the data says nothing, the streets stay d
56
60
 
57
61
  The map describes its street lights in its roads module, next to the other details drawn from
58
62
  street furniture, and publishes them with its style for every theme; `stats.declared` tells whether
59
- it does. Without a description the lights take the defaults below:
63
+ it does. Without an enabled description the renderer stays off and mounts no layer:
60
64
 
61
65
  <!-- docs:check -->
62
66
 
63
67
  ```ts
64
- import {defineMap, refine} from '@tileflow/core';
68
+ import {defineMap, fixed, refine} from '@tileflow/core';
65
69
  import {streets} from '@tileflow/maps';
66
70
 
67
71
  export default defineMap({
@@ -71,7 +75,14 @@ export default defineMap({
71
75
  extends: streets,
72
76
  modules: {
73
77
  // Sodium-orange light, a little brighter, on every street of tiles that map few lamps.
74
- roads: refine({streetLights: {color: '#ffb35c', intensity: 1.3, estimate: 'streets'}}),
78
+ roads: refine({
79
+ streetLights: {
80
+ enabled: true,
81
+ color: fixed('#ffb35c', {reason: 'Constant sodium-lamp color.'}),
82
+ intensity: 1.3,
83
+ estimate: 'streets',
84
+ },
85
+ }),
75
86
  },
76
87
  });
77
88
  ```
@@ -89,11 +100,16 @@ street rather than as lamps that may not stand there.
89
100
 
90
101
  `color` is the light of the lamps whose data gives no colour, a warm white by default, and
91
102
  `intensity` scales the light from 0 to 2 (1 by default); both may be theme tokens and mix during a
92
- blend. `streetLights: {enabled: false}` gives the map none.
103
+ blend. These settings apply only after `enabled: true`; `streetLights: {enabled: false}` gives
104
+ the map none.
93
105
 
94
106
  ## Options
95
107
 
96
- The layer's own options say only how it is delivered: `minzoom` (14 by default) and `maxTiles` (25
108
+ The layer follows the compiled map by default. `createTileflowStreetLights({map, controller,
109
+ enabled: true})` explicitly enables the fallback design for a style without street-light metadata;
110
+ `enabled: false` keeps the renderer off even when the map declares lights.
111
+
112
+ The delivery options are `minzoom` (14 by default) and `maxTiles` (25
97
113
  by default, at most 64) bound where and how far the lights shine, `beforeId` places the layer before
98
114
  another one, and `transformRequest` applies the map's request rules to the tiles it reads.
99
115
 
@@ -15,8 +15,8 @@ application's own WebGL layers to draw, from the values `getSun()` returns.
15
15
  ## Describe a theme's sun
16
16
 
17
17
  Set `lighting.sun` and `lighting.ambient` in the theme. Every value is optional; unset values take
18
- the defaults in `tileflowDefaultSunPreset` (azimuth 210°, altitude 60°, white light at 0.5, full
19
- shadows, a white sky at 0.5):
18
+ the defaults in `tileflowDefaultSunPreset` (azimuth 210°, altitude 60°, white light at 0.2, full
19
+ shadow strength, a white sky at 0.8 for soft daylight shadows):
20
20
 
21
21
  <!-- docs:check -->
22
22
 
@@ -43,6 +43,46 @@ others are inherited. The compiled style carries the resolved sun in its metadat
43
43
  direction in `lighting.position` no longer apply to a theme with a sun. A theme without `sun` or
44
44
  `ambient` compiles exactly as before.
45
45
 
46
+ ## Replace inherited lighting
47
+
48
+ `defineTheme(base, definition)` normally merges lighting field by field. An empty `lighting`,
49
+ `sun`, or `ambient` object therefore preserves inherited values. Use the existing `reset()`
50
+ operation to remove a preset explicitly while keeping the base theme's palette and typography:
51
+
52
+ <!-- docs:check -->
53
+
54
+ ```ts
55
+ import {defineTheme, reset} from '@tileflow/core';
56
+ import {streetsThemes} from '@tileflow/maps';
57
+
58
+ export const studio = defineTheme(streetsThemes.light, {
59
+ id: 'studio',
60
+ version: 1,
61
+ colorScheme: 'light',
62
+ lighting: {
63
+ sun: reset(),
64
+ ambient: reset(),
65
+ anchor: 'viewport',
66
+ position: [1.15, 180, 40],
67
+ color: '#FFFFFF',
68
+ intensity: 0.15,
69
+ },
70
+ });
71
+ ```
72
+
73
+ Removing both presets lets the native extrusion light use the authored `anchor` and `position`.
74
+ Resetting only `sun` keeps an inherited ambient preset, and resetting only `ambient` keeps the sun;
75
+ the missing counterpart then uses the SDK defaults. `lighting: reset()` clears all inherited
76
+ lighting properties. A direct property such as `position`, `color`, or `intensity` can also be
77
+ reset. The result is a complete theme with those properties absent; it contains no reset nodes.
78
+ To replace a whole sun or ambient object, first derive a theme that resets that object, then derive
79
+ the replacement values from the cleared theme.
80
+
81
+ Removing a preset does not turn off the renderer's lighting, cast shadows, or ambient occlusion.
82
+ The SDK's 3D scene uses its fallback light when no preset is published. For a graphic building
83
+ style, choose the building module's `lighting: 'style'`, `shadows: false`, and `occlusion: false`
84
+ explicitly. A single static theme needs no time-of-day family or running clock.
85
+
46
86
  ## Read the sun in an application
47
87
 
48
88
  `themeController.getSun()` returns the sun the map shows: