@tileflow/core 0.1.0-alpha.34 → 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.
Files changed (36) hide show
  1. package/README.md +24 -23
  2. package/THIRD_PARTY_NOTICES.md +100 -6
  3. package/dist/browser.d.ts +758 -388
  4. package/dist/browser.js +4598 -1169
  5. package/dist/build.d.ts +9 -5
  6. package/dist/build.js +2892 -1410
  7. package/dist/capture.js +1 -0
  8. package/dist/config-reference.json +3780 -2153
  9. package/dist/{icon-composition-DqoZWQuz.d.ts → icon-composition-sN3JkXdb.d.ts} +380 -96
  10. package/dist/index.d.ts +426 -13
  11. package/dist/index.js +4999 -1841
  12. package/dist/{manifest-types-hmzy2bBX.d.ts → manifest-types-jrBdBrES.d.ts} +71 -9
  13. package/dist/manifest.d.ts +2 -2
  14. package/dist/manifest.js +80 -2
  15. package/dist/native-profile.js +1 -0
  16. package/dist/native.d.ts +13 -3
  17. package/dist/native.js +714 -318
  18. package/dist/runtime.d.ts +1 -1
  19. package/dist/runtime.js +124 -6
  20. package/dist/worker/buildings-3d-worker.global.js +1 -0
  21. package/docs/authoring-model.md +12 -3
  22. package/docs/boundary-areas-and-choropleths.md +217 -0
  23. package/docs/buildings-3d.md +264 -34
  24. package/docs/cartographic-defaults.md +142 -0
  25. package/docs/compiled-style-performance.md +14 -3
  26. package/docs/data-is-separate-from-design.md +191 -7
  27. package/docs/hosted-session-authorization.md +2 -4
  28. package/docs/map-and-terrain-examples.md +10 -3
  29. package/docs/native-artifact-profile.md +5 -0
  30. package/docs/native-resource-urls.md +5 -3
  31. package/docs/street-lights.md +22 -6
  32. package/docs/sun-and-light.md +42 -2
  33. package/docs/theme-transitions-and-blends.md +50 -0
  34. package/docs/themes-and-module-styles.md +66 -0
  35. package/docs/trees-3d.md +85 -13
  36. package/package.json +5 -4
@@ -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
 
@@ -295,10 +295,181 @@ 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
+ 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
+
463
+ Water-module styles override product-level `bands`, `contours`, and `labels`. Existing custom
464
+ `text.field` expressions remain authoritative: when refining a map from legacy polygons to native
465
+ contours, use `expr.get(field('bathymetryContourDepth'))` for its depth. That auxiliary binding
466
+ resolves to `depth` independently of the primary World schema; `bathymetryMinDepth` keeps its
467
+ legacy band meaning. Raw `expr.get('depth')` is rejected by authoring validation.
468
+ Increasing only `maxZoom` does not remove an opacity curve that fades out earlier. Extend ranges
469
+ and opacity only where the selected source supports the intended detail.
470
+
471
+ The V1 schema also reserves `bathymetry_coverage`; Core adds no coverage rendering by default.
472
+ Derived contours and names provide geographic context, not certified navigation data.
302
473
 
303
474
  The DEM enables future client-side sampling and depth profiles, but Core intentionally exposes no
304
475
  profile or remote sampling API yet. Rendered relief must not be interpreted as navigation-safe
@@ -318,6 +489,15 @@ explicit because the data contract cannot assume a sprite name. They require pos
318
489
  `markings` evidence and reject `crossing=no`; both fields are semantic bindings, so custom schemas
319
490
  can remap them without weakening that rule.
320
491
 
492
+ Metric road rings scale continuously with the map from zoom 15 through 24, including fractional
493
+ zooms. Their casing is below surface road fills so approaching streets cover the outline at each
494
+ junction. The ring fill stays below bridges and road markings.
495
+
496
+ Road treatment `widthScale` scales both the stroke and an authored `gapWidth`, keeping hollow
497
+ casings aligned with the carriageway at fractional zoom levels.
498
+ When every dash treatment is continuous (`[1, 0]`), road layers omit the dash pattern so thin
499
+ casings render as solid strokes. Actual construction and tunnel dashes remain patterned.
500
+
321
501
  Datasets may also publish the optional, decorative `canopy` layer: points inside urban parks and
322
502
  gardens, at most one per cell of a grid of about 20 m that measured tree canopy covers. Each point
323
503
  tells how much room a drawn grove of tree crowns has and how dense the canopy around it is. The
@@ -421,7 +601,11 @@ buildings. Its `activity_score`, `rank`, `min_zoom`, and `confidence` fields con
421
601
  warm tint without drawing POI-radius circles. Current building footprints may carry sparse
422
602
  `building_tone=commercial|destination|active`; absence means the neutral base building. The three
423
603
  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
604
+ Buildings may also carry World's `class`, their use in Overture's building subtype values; without
605
+ a tone, the buildings module colours `residential`, `commercial`, public buildings (`civic`,
606
+ `education`, `medical`, `religious` and the like) and works (`industrial`, `agricultural`,
607
+ `service`) with the theme's `residential`, `commercial`, `civic` and `industrial` building colours,
608
+ and every other building with `generic`. Building visibility is fixed by the layer zoom contract and never by optional semantics. The land
425
609
  module also exposes
426
610
  `medical`, `education`, and `government` independently instead of collapsing their already distinct
427
611
  source classes into one civic color.
@@ -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
@@ -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
 
@@ -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:
@@ -258,3 +258,53 @@ export async function duskWhileFlying(
258
258
  The React, Vue, and Svelte maps take the same flag in `themeBlend`: pass
259
259
  `themeBlend={{hold: true, position, themes}}` while the camera moves and drop `hold` when it rests.
260
260
  The React Native `Map` blends natively and does not take `hold`.
261
+
262
+ ## Time of day
263
+
264
+ `timeOfDay` is an optional map capability. It maps a theme and an artistic preset (`day`,
265
+ `dawn`, `dusk`, `night`) to a complete theme already in `themes`:
266
+
267
+ <!-- docs:check -->
268
+
269
+ ```ts
270
+ import {defineMap} from '@tileflow/core';
271
+ import {streets, streetsThemes} from '@tileflow/maps';
272
+
273
+ export default defineMap({
274
+ id: 'city',
275
+ version: 1,
276
+ extends: streets,
277
+ themes: {
278
+ paper: streetsThemes.light,
279
+ sunrise: streetsThemes.dawn,
280
+ evening: streetsThemes.dusk,
281
+ ink: streetsThemes.dark,
282
+ },
283
+ defaultTheme: 'paper',
284
+ systemThemes: {light: 'paper', dark: 'ink'},
285
+ timeOfDay: {
286
+ paper: {day: 'paper', dawn: 'sunrise', dusk: 'evening', night: 'ink'},
287
+ },
288
+ });
289
+ ```
290
+
291
+ Applications keep `theme` and optionally add `timeOfDay`: `theme="paper" timeOfDay="dusk"`
292
+ selects `evening`. Compilation supports `createStyle(map, {theme: 'paper', timeOfDay: 'dusk'})`;
293
+ `resolveTileflowRuntimeTheme(map, theme, colorScheme, timeOfDay)` uses the same resolution.
294
+ Omitting the preset preserves the existing selection. `system` resolves its light/dark theme
295
+ first. A concrete variant such as `ink` also identifies its family when a preset is supplied.
296
+ Maps or themes without a family ignore this option. Within a declared family, an unavailable
297
+ preset is an error; controls should offer only declared presets. Families must include their own
298
+ name in their presets, and a concrete theme can belong to only one family.
299
+
300
+ The catalog inherits atomically; `timeOfDay: {}` disables it on an extension. Replacing `themes`
301
+ clears an inherited catalog. `defaultTheme`, `systemThemes`, and `themeTransition` retain their
302
+ behavior. An explicit `themeBlend` owns its concrete theme sequence and takes precedence over
303
+ `timeOfDay`. Changing the preset does not start a clock or call `setSun`; lighting is whatever the
304
+ selected theme already declares, and an application's sun override retains its existing behavior.
305
+ Captures, style URLs, and transition events use the resolved concrete theme name.
306
+
307
+ Maps with a catalog publish runtime manifest version 2. Updated browser and native readers accept
308
+ both versions. Old readers need a version 1 manifest: `createManifest(project, {manifestVersion: 1})`
309
+ omits the catalog and keeps every concrete theme available. Maps without a catalog still emit
310
+ version 1. Update readers before delivering a version 2 manifest.
@@ -15,6 +15,61 @@ same token keys, so changing a theme can never change map structure or silently
15
15
  role. `defineTheme(base, definition)` is an authoring convenience that returns a fully materialized
16
16
  document; it does not leave an inheritance edge in the resolved config.
17
17
 
18
+ ## Start with one appearance
19
+
20
+ A map can have one appearance for its entire lifetime. Neither `systemThemes` nor `timeOfDay` is
21
+ required. This complete theme supplies the twelve base color roles the semantic compiler needs;
22
+ copy it and change its colors to start an independent design without inheriting an official map:
23
+
24
+ <!-- docs:check -->
25
+
26
+ ```ts
27
+ import {defineTheme} from '@tileflow/core';
28
+
29
+ export const paper = defineTheme({
30
+ id: 'paper',
31
+ version: 1,
32
+ colorScheme: 'light',
33
+ tokens: {
34
+ color: {
35
+ 'surface.background': '#F6F7F3',
36
+ 'surface.land': '#F1F3ED',
37
+ 'surface.water': '#A9D3F5',
38
+ 'surface.park': '#CDE8B5',
39
+ 'surface.building': '#E6E3DA',
40
+ 'roads.default': '#FFFFFF',
41
+ 'roads.major': '#F4C95D',
42
+ 'roads.casing': '#D9DEE2',
43
+ 'boundaries.default': '#C9D1D9',
44
+ 'labels.primary': '#3C4043',
45
+ 'labels.muted': '#727B84',
46
+ 'labels.halo': '#FFFFFF',
47
+ },
48
+ },
49
+ });
50
+ ```
51
+
52
+ An existing map definition can select this appearance with `themes: {paper}` and
53
+ `defaultTheme: 'paper'`. Supply that map's data and font assets separately; the default typography
54
+ uses `Noto Sans Regular`. A theme without these base roles is a valid appearance document but cannot
55
+ compile a semantic map. Colors above are an editable starting point, including for monochrome,
56
+ historical, dark-only, or imagined worlds; `colorScheme` describes the appearance for consumers and
57
+ does not generate a palette or choose a time of day.
58
+
59
+ The compiler derives more specific roles when they are absent. For example, primary and motorway
60
+ roads initially use `roads.major`; declaring `roads.motorway` replaces only that derived color.
61
+ Land-use fills, road casings, and label colors have their own semantic roles, so an explicit role
62
+ always wins over its fallback. Use `color.mix()` and `token.color()` in a theme when the relationship
63
+ itself should be authored and inspectable. Derivations resolve during compilation, without a clock
64
+ or a per-frame palette calculation.
65
+
66
+ Add named appearances when the map needs them. A day/night pair does not need dawn or dusk, and a
67
+ theme named `paper` or `fantasy` does not need a temporal family. Declare `timeOfDay` only when the
68
+ application should offer those artistic moments; see
69
+ [time-of-day selection](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md#time-of-day).
70
+
71
+ ## Compose and replace appearance values
72
+
18
73
  Some optional tokens are for the application rather than the map's own layers. Route inks
19
74
  (`route.line`, `route.casing`, `route.core`, `route.glow`, `route.alternative`,
20
75
  `route.glow.opacity` and `route.core.opacity`) set how an application draws its routes; a theme
@@ -83,6 +138,17 @@ deterministic OKLCH `color.mix()` keep derived palette logic inspectable. Unknow
83
138
  cross-category refs, token-schema drift, and unresolved visual nodes fail before Style JSON is
84
139
  emitted.
85
140
 
141
+ Theme lighting also supports explicit removal during `defineTheme` composition. Use
142
+ `lighting: reset()` to remove the entire inherited lighting configuration, or
143
+ `lighting: {sun: reset(), ambient: reset()}` to remove those presets while retaining other light
144
+ properties. The same operation can remove a direct property such as `position` or `color`.
145
+ Omitting a property, or supplying an empty object, continues to inherit it. Reset operations are
146
+ materialized immediately and never enter the resolved theme or Style JSON. Removing a lighting
147
+ preset restores the renderer's fallback; it does not disable 3D shadows or occlusion. Those remain
148
+ explicit building settings. See
149
+ [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md#replace-inherited-lighting)
150
+ for a complete example.
151
+
86
152
  There is no physical-layer override for the Tileflow basemap or separate official-map authoring
87
153
  surface. Basemap behavior belongs to a typed control or owner-local render stack. Every
88
154
  official and application map therefore follows the same schema, inheritance rules, diagnostics,