Reference
Mapbox Style Spec coverage.
Single source of truth for what the convertMapboxStyle pipeline handles. Each row is checked against the converter source at build time — entries marked supported must have a matching reference in the converter; new converter cases that miss a table entry fail CI. See the migration guide at Mapbox migration for narrative context and examples.
Summary
Reading this table
Supported — converter emits an xgis form AND the runtime honours it. Partial — converter emits SOMETHING but loses information (e.g. exponential interpolation folded to linear) or the runtime side has a gap. Unsupported — silently dropped or warned. N/A — Mapbox-only concept with no xgis equivalent and no plan to add.
Impact tier reflects user-visible severity in common basemap styles (OFM Bright, MapLibre demo), not effort to fix.
Top-level style properties
Fields on the root Mapbox style object.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
version | n/a | — | Spec versioning; ignored. | — |
name | supported | — | Emitted as a leading /* comment */ in the converted xgis. | mapbox-to-xgis.ts |
metadata | unsupported | low | Silent drop — informational only in Mapbox. | — |
center | supported | — | Applied by the demo-runner Mapbox importer after `runSource()` via `Camera.centerX/Y` + `markCameraPositioned()`. URL-hash camera still wins (hash parsing runs first). Compiler does NOT encode camera state into xgis source — top-level camera lives in the runtime, not the DSL. | — |
zoom | supported | — | Same path as `center` — runtime-side via demo-runner. | — |
bearing | supported | — | Same path as `center` — runtime-side via demo-runner. | — |
pitch | supported | — | Same path as `center` — runtime-side via demo-runner. | — |
sources | supported | — | — | sources.ts |
layers | supported | — | — | layers.ts |
sprite | supported | — | Importer extracts the URL from raw JSON and forwards to XGISMap.setSpriteUrl(). Runtime IconStage fetches `${url}.json` + `${url}.png` (DPR>=1.5 tries `@2x` first) and renders bitmap icons; SDF icons + icon-text-fit are Phase 2. Unknown icon names dropped silently at prepare-time; iter 526 added IconStage.getMissingIconNames() diagnostic for post-load misses. | — |
glyphs | supported | — | Importer extracts the URL from raw JSON and forwards to XGISMap.setGlyphsUrl(). Runtime TextStage fetches MapLibre SDF PBFs and upgrades visually when available; Canvas2D fallback stays on for offline / missing-glyph cases. Not encoded in xgis source. | — |
transition | unsupported | low | Per-property fade-in dropped. | — |
light | supported | low | WS-9 — custom `light` (position / intensity / color) is now honoured. The extrude shader (vs_main_ecef_extruded) reads intensity from light_dir_ecef.w and colour from light_color_packed (RGBA8) instead of the old baked WGSL consts; the CPU packs the MapLibre default (position [1.15,210°,30°] → (0.288,-0.498,0.996), intensity 0.5, white) when no light is authored, so the default render is byte-identical. Host-applied like projection/camera: the demo-runner + compare-runner parse the top-level `light` block and call XGISMap.setLight(), which the render loop pushes into every VTR each frame. `anchor` is accepted but the directional frame stays the #420 camera-anchor ENU basis (the map/viewport bearing distinction is not yet modelled — invisible on the target corpus, which uses the default anchor). Light affects fill-extrusion only. | — |
fog | unsupported | low | Mapbox v3 distance-fog gradient. Would need a post-process pass with depth-based mixing. | — |
terrain | unsupported | medium | Roadmap Batch 4 (raster-dem + hillshade). | — |
projection | supported | low | WS-8 — all 8 X-GIS projections (mercator / equirectangular / natural_earth / orthographic / azimuthal_equidistant / stereographic / oblique_mercator / globe) render and the top-level style-spec `projection` field is now honoured. Same host-integration path as center/zoom: the playground demo-runner + compare-runner read the raw style JSON, map the Mapbox type name (e.g. globe → globe, naturalEarth → natural_earth via setProjection ALIASES) and call XGISMap.setProjection() after runSource(). URL `?proj=` still overrides. Mapbox-only types with no X-GIS equivalent (albers / equalEarth / lambertConformalConic / winkelTripel) warn at setProjection and keep the current projection. Not encoded in the xgis DSL (runtime-only, by design). | — |
imports | unsupported | — | Mapbox v3 style-import not parsed. | — |
Source types
`sources[id].type` values.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
vector (.pmtiles) | supported | — | Routed to PMTilesBackend. | sources.ts:38 |
vector (TileJSON) | supported | — | Runtime fetches manifest then attaches PMTiles backend. | sources.ts:41 |
pmtiles | supported | — | Community-extension type ("type":"pmtiles") accepted as a sibling of the .pmtiles-URL detection path. | sources.ts:94 |
tilejson (explicit) | supported | — | Third-party convention: `"type":"tilejson"` directly. Routed alongside the `vector` + URL-sniffing path. | sources.ts:105 |
raster | supported | — | — | sources.ts:48 |
geojson (URL) | supported | — | — | sources.ts:73 |
geojson (inline) | supported | — | Captured via inlineGeoJSON collector → auto-pushed after run(). | sources.ts:77 |
raster-dem | partial | medium | #777 Phase II — source threads encoding / tileSize (+ custom unpack factors); the DEM is fetched + RGBA8-decoded (mapbox / terrarium / custom) in the HillshadeRenderer. End-to-end relief draw pending pass wiring + real-GPU A/B (INC-5/6); terrain vertex displacement is future (II6). | sources.ts |
image | unsupported | low | Single-image source (e.g. user-supplied PNG draped onto a quad). Not in current loader; raster is the closest substitute. | — |
video | unsupported | low | Streaming video source. Not in current loader. | — |
Layer types
`layer.type` values.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
background | supported | — | Lifts to top-level `background { fill: # }` directive. | mapbox-to-xgis.ts:82 |
fill | supported | — | — | — |
line | supported | — | — | — |
symbol (text) | supported | — | TextStage renders SDF glyphs from Canvas2D fonts. | layers.ts:154 |
symbol (icon-only) | partial | medium | Icon-only symbol layers (no text-field) route to the icon stage (#777 I1/I2, PR #965): constant `icon-image` → `label-icon-image-<name>`; data-driven `icon-image: ["match"|"coalesce"|["image", …]]` → per-feature `label-icon-image-[<expr>]` → IconStage.addIcon. Still partial: the icon LAYOUT tail (icon-text-fit / icon-padding / icon-keep-upright / icon-pitch-alignment) and text/icon halo are deferred to the Phase I remainder. | layers-symbol.ts:252 |
fill-extrusion | supported | — | Extruded polygon with per-vertex z. | — |
raster | supported | — | — | — |
circle | supported | — | Routes to the runtime PointRenderer (SDF disks). circle-radius/-color/-stroke-color/-stroke-width/-opacity all map onto the existing point utility surface, including interpolate-by-zoom + data-driven forms. | layers.ts:514 |
heatmap | supported | — | Phase R — 3-pass GPU pipeline (accum → Gaussian blur → density→colour compose) in HeatmapRenderer. Routes GeoJSON-source Point/MultiPoint heatmap layers to the renderer; heatmap-radius/-weight/-intensity/-color/-opacity supported. Tile-sourced heatmaps deferred. | layers-heatmap.ts |
hillshade | partial | medium | #777 Phase II — registered converter (paint-hillshade.ts) + DSL fs_hillshade (DEM decode → Sobel → standard/basic shade) + HillshadeRenderer/Draper landed (INC-1..3). Single-source constant paint only; multidirectional + resampling:linear smoothing + cross-tile edge backfill deferred. End-to-end draw pending pass wiring + real-GPU A/B (INC-5/6). | layer-converters/generic.ts |
sky | unsupported | low | Atmospheric sky dome (sky-color / sky-atmosphere-* / sky-type). Layer-level skip added to SKIP_REASONS so the converter emits an explicit // SKIPPED comment with diagnostic note rather than falling through to the generic handler. | layers.ts:SKIP_REASONS |
Layer common fields
Shared across all `layer` shapes regardless of type.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
id | supported | — | Sanitised into a valid xgis identifier. | layers.ts:520 |
type | supported | — | Discriminator — see Layer types table above. | — |
source | supported | — | — | layers.ts:521 |
source-layer | supported | — | Lowered to `sourceLayer: "..."` block prop. | layers.ts:522 |
minzoom | supported | — | PR #81: enforced at every label submission via `inZoomRange`. | layers.ts:523 |
maxzoom | supported | — | — | layers.ts:524 |
filter | supported | — | Legacy + expression form; routes through filter-eval. | layers.ts:525 |
metadata | n/a | — | na — informational only; the Mapbox spec defines `metadata` as arbitrary author data that does NOT affect rendering, so there is nothing to render. Silently dropped (no visual effect). | — |
ref | n/a | — | Deprecated layer-ref shorthand (Mapbox style spec v7). | — |
Layout — fill / line
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
visibility | supported | — | `none` → `visible: false`. | layers.ts:538 |
line-cap | supported | — | butt / round / square literals only. | layers.ts:548 |
line-join | supported | — | miter / round / bevel literals only. | layers.ts:552 |
line-miter-limit | supported | — | Constant only. | layers.ts:556 |
line-round-limit | supported | — | Per-layer round-join fold threshold (default 1.05). Threaded end-to-end: layout `line-round-limit` → `stroke-roundlimit-N` → StrokeNode.roundLimit → ShowCommand → line layer uniform slot → the line shader scales its round-join acute-fold threshold by round-limit / 1.05. UNSET / default reproduces today's geometry byte-for-byte (the shader keeps its historical fold constant when the uniform is 0). | layer-converters/line.ts:48 |
fill-sort-key | n/a | — | na — incompatible with X-GIS' single-merged-mesh-per-tile draw model. X-GIS tessellates every fill feature of a layer into ONE merged mesh per tile (no per-feature draw loop), so a CONSTANT sort-key (the only form seen in practice) is a no-op BY CONSTRUCTION (uniform key → stable sort = source order = today's pixels), and honouring the data-driven form would require re-architecting the merged-mesh into a per-feature index-buffer reorder in the tiler's CPU↔WGSL byte-contract packing path — a change the single-draw perf design deliberately precludes. symbol-z-order — the tractable sibling on the per-feature symbol collision/draw pass — is shipped (Phase S Batch 4). | — |
line-sort-key | n/a | — | na — same single-draw-architecture incompatibility as fill-sort-key: line features share ONE packed segment buffer per tile, so a constant sort-key is a no-op by construction and the data-driven form would need the merged-buffer reorder the single-draw design precludes. | — |
circle-sort-key | n/a | — | na — same single-draw-architecture incompatibility as fill-sort-key: PointRenderer draws the layer's circles from ONE shared instance buffer, so a constant sort-key is a no-op by construction and the data-driven form would need a per-feature instance reorder the single-draw design precludes. (Sibling symbol-z-order shipped Phase S Batch 4.) | — |
Layout — symbol
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
symbol-placement | supported | — | point / line / line-center literals; `["step", ["zoom"], …]` form expands to multiple layers with intersected minzoom/maxzoom + segment-resolved placement (OFM Bright highway-shield-* coverage). Non-zoom step inputs fall back to default placement. | layers.ts:447 |
symbol-spacing | supported | — | Defaults to 250 px when missing on line placement. | layers.ts:471 |
symbol-avoid-edges | n/a | — | na — MOOT for X-GIS' label model. Skip labels whose bbox crosses a tile boundary (de-dupe at tile seams): the tile-seam duplication this property works around does not ARISE in X-GIS, whose placement/collision runs frame-globally in screen space over cross-tile-deduped anchors, not per-tile. Faithful support is therefore not merely unimplemented but inapplicable to the architecture: the placement/collision pass (runtime text-stage.ts) operates entirely in SCREEN pixels with no tile coordinate frame (AGENTS: "projection is the caller's responsibility"), and the upstream label source (label-feature-source.ts forEachLabel) emits each feature as a single absolute-Mercator anchor deduped across tiles — the stride-13 point-vertex format carries ECEF + abs-lon/lat + abs-Mercator, NOT tile-local coords or the tile extent. A faithful tile-edge-crossing test needs the label's bbox in tile-local units against [0, EXTENT], which would require threading tile-local coords + extent through the VTR point-vertex format, forEachLabel, and addLabel (cross-subsystem, high blast radius). The converter RECOGNISES the property and emits a precise warning (layers-symbol.ts convertGapWarnings) noting the knob is moot for X-GIS' cross-tile-collision model. Warned, not supported. | — |
symbol-sort-key | partial | medium | Constant numeric value plumbed end-to-end (iter 399-405). Runtime collision pass sorts CollisionItems by sortKey ascending — lower wins. Expression form (`["get", "rank"]`) flattens to 0 with a warning. | layers.ts:702 |
symbol-z-order | supported | — | Enum auto / viewport-y / source. Threaded end-to-end: layout `symbol-z-order` → `label-z-order-<v>` → LabelDef.symbolZOrder → TextStage.prepare() ordering pass. `viewport-y` orders labels by screen Y (lower-on-screen placed first in collision → drawn on top); `source` keeps source/feature order (suppresses the reverse-layer trick); `auto` (and unset) reproduce X-GIS' historical reverse-layer / sortKey ordering BYTE-FOR-BYTE — only an explicit `viewport-y` / `source` activates the new sort. Applied across the frame's aggregated label set (X-GIS collides all symbol layers together); per-layer-scoped viewport ordering of mutually-colliding layers is an approximation. | layers-symbol.ts:1079 |
text-field | supported | — | String / {token} / expression / number / boolean / null. Colon-bearing locale keys route via `get("name:xx")`. | layers.ts:164 |
text-font | supported | — | Family extracted, weight + italic stripped into `label-font-weight-N` / `label-italic`. | layers.ts:417 |
text-size | supported | — | Constant + interpolate-by-zoom + per-feature expression (sizeExpr). | layers.ts:231 |
text-max-width | supported | — | Default 10 ems for non-line placement (Mapbox parity). | layers.ts:385 |
text-line-height | supported | — | — | — |
text-letter-spacing | supported | — | Constant + interpolate-by-zoom. | — |
text-justify | supported | — | auto / left / center / right literals. | — |
text-anchor | supported | — | Full 9-way (center / top / bottom / left / right + 4 diagonals). | layers.ts:295 |
text-variable-anchor | supported | — | Real layout property (and legacy array-in-text-anchor) lower to anchorCandidates; runtime collision picks first non-overlapping. | layers.ts:370 |
text-variable-anchor-offset | supported | — | Per-anchor em offsets; runtime applies MapLibre baseline shift. | layers.ts:435 |
text-radial-offset | supported | — | Constant em; runtime fromRadialOffset per candidate anchor (MapLibre-parity). | layers.ts:435 |
text-offset | supported | — | Constant 2-tuple only. | layers.ts:329 |
text-rotate | supported | — | Constant only. | — |
text-padding | supported | — | Constant + interpolate-by-zoom. | layers.ts:351 |
text-transform | supported | — | uppercase / lowercase / none literals. | — |
text-allow-overlap | supported | — | — | — |
text-ignore-placement | supported | — | — | — |
text-overlap | partial | low | MapLibre overlap-policy enum (never / always / cooperative). always → label-allow-overlap; never → default; cooperative approximated as always (priority-aware collision pending) + warning. Wins over legacy text-allow-overlap when both declared. | layers.ts:418 |
text-optional | unsupported | low | Icons not implemented — moot. | — |
text-rotation-alignment | supported | — | Literal map / viewport / auto. Honoured at runtime. | map.ts:2369 |
text-pitch-alignment | partial | medium | Converter emits, runtime ignores — labels never project onto ground plane. Iter 10 surfaced an explicit warning when `map` is authored (the gap-revealing case) so authors of pitched-view styles see the diagnostic. `viewport` and `auto` match X-GIS' billboard-rendering default and stay silent. | map.ts:2461 |
text-keep-upright | supported | — | Per-glyph flip for line labels. | text-stage.ts:509 |
text-writing-mode | unsupported | medium | CJK vertical text would need a per-glyph rotation pipeline. | — |
text-max-angle | supported | — | Max degrees between adjacent glyphs on a line-placed label. Threaded end-to-end: layout `text-max-angle` → `label-max-angle-N` → LabelDef.maxAngle → TextStage curved-loop angular gate (drops the label when the wrapped per-glyph tangent delta exceeds the threshold). UNSET = no clamp (X-GIS historical behaviour preserved byte-for-byte for styles that don't author it); authoring a value (incl. spec default 45) activates the gate. | layers-symbol.ts:1072 |
icon-image | supported | high | Constant + data-driven match/case via label-icon-image-[<expr>] bracket binding. Per-feature evaluation in TextStage.applyFeatureExprs dispatches IconStage.addIcon. Iter 490 + 491 shipped 2026-05-18. Iter 535 verified end-to-end across the OFM Bright highway-shield path (road_N / us-interstate_N / us-state_N): the iter 531 null-comparison fix unblocks the shield-layer filter, the diagnostic quartet (iter 526/532/533/534) confirmed dispatch → vertex buffer → GPU draw all complete. The atlas ships shields as WHITE-on-transparent backgrounds (zero SDF sprites) so colored shield appearance comes from the text-field number overlay — not sprite tinting. Icon-ONLY layers (icon-image, no text-field) route the same way for a data-driven icon-image too: `["get"]` / `["match"]` / `["coalesce", ["image", …]]` no longer drop at convert (#777 I1) — the empty-text label carries iconImageExpr to the same per-feature IconStage dispatch. | layers.ts:1007 + map.ts:applyFeatureExprs + layer-converters/symbol.ts |
icon-size | supported | — | Constant + zoom-interp (iter 523) + data-driven (#777 I-F). Bracket-binding `label-icon-size-[interpolate(zoom, …)]` lowers to LabelShapes.iconSize zoom-interpolated PropertyShape (runtime resolveNumberShape at dispatchIcon); `label-icon-size-[<case/match/get>]` (converter exprToXgis) lowers to a data-driven PropertyShape that applyFeatureExprs (label-pass.ts) evaluates per feature (clamped >= 0) → per-vertex quad width. OFM bright road_oneway / road_oneway_opposite (15→0.5, 19→1) honoured. | layers.ts:1075 |
icon-rotate | supported | — | Constant degrees. | layers.ts:641 |
icon-anchor | supported | — | Literal 9-way enum. | layers.ts:627 |
icon-offset | supported | — | [x, y] in CSS px; split into label-icon-offset-x / -y utilities. | layers.ts:631 |
icon-allow-overlap | supported | medium | `true` (and absent) = X-GIS' historical always-place — OFM label_city/town/village/city_capital authoring `true` (4 layers/fixture) render unchanged. `false` now threads to the icon collision queue: layers-symbol emits `label-icon-collide` → LabelDef.iconCollide → dispatchIcon sets the IconStage `collide` AABB (#417/#419), dropping an icon whose padded box overlaps an already-placed icon. DEFAULT note: the spec default is `false`(collide), but the ABSENT default stays always-place (byte-identical to today, OFM density-safe); only an explicit `false` opts in. icon-overlap wins when both declared. Phase S Batch 4. | layers-symbol.ts:777 + label-pass.ts:dispatchIcon |
icon-overlap | supported | medium | MapLibre overlap-policy enum. `always` = X-GIS always-place default. `never` → icon collision queue (label-icon-collide → IconStage `collide` AABB). `cooperative` approximated as `never` (collide) + warning (no priority-aware icon arbitration). Wins over legacy icon-allow-overlap when both declared. Phase S Batch 4. | layers-symbol.ts:777 + label-pass.ts:dispatchIcon |
icon-ignore-placement | supported | medium | `true` → the icon places AND does not block others: emits `label-icon-ignore-placement` → LabelDef.iconIgnorePlacement → dispatchIcon keeps the icon OUT of the collide queue, overriding an explicit icon-overlap:never/icon-allow-overlap:false back to always-place-and-don't-block (X-GIS' default for a non-colliding icon). `false` (default) is a no-op. Phase S Batch 4. | layers-symbol.ts:813 + label-pass.ts:dispatchIcon |
icon-optional | supported | low | `true` → a colliding icon may hide while its paired text still shows: emits `label-icon-optional` → LabelDef.iconOptional. X-GIS' text is an independent TextStage symbol, so a policy-dropped icon already never cascades to its paired text — `true` is honoured. DEFAULT `false` (drop the paired text WITH the icon) needs the reverse icon→text collision signal (not wired) and is reported deferred; emitting nothing for the default keeps it byte-identical. Phase S Batch 4. | layers-symbol.ts:862 |
icon-rotation-alignment | supported | medium | All three values (map / viewport / auto) honored. "viewport"/"auto" map to X-GIS axis-aligned icons; "map" adds the per-segment tangent to icon-rotate at dispatch time under symbol-placement=line (OFM road_oneway one-way arrows). Compiler iter 506 emits label-icon-rotation-alignment-map; runtime adds tangent in dispatchIcon. | layers.ts:1056 + map.ts:dispatchIcon |
icon-padding | supported | low | Per-icon collision-bbox padding (layout, px, default 2). Constant form lowered (#777 I-D): converter emits label-icon-padding-N for non-default values only → LabelDef.iconPadding → dispatchIcon → IconStage collision box replaces the fixed 2*dpr. Non-constant (zoom-interp / data-driven) forms deferred; data-driven → cluster I-F. | — |
icon-text-fit | supported | medium | Shield/badge backgrounds: the icon sprite quad stretches to wrap the PAIRED label's shaped text bbox. Constant enum lowered (#777 I-A): converter emits label-icon-text-fit-<width|height|both> → LabelDef.iconTextFit → label-pass dispatchIcon passes the fit to IconStage → IconStage.prepare reads the paired text bbox TextStage laid out this frame (pairKey → getPairFitBoxes) → icon-renderer stretches drawW/drawH before anchorOffset. `none`/absent = native sprite size (byte-identical). Non-constant (expression) forms warn once. | — |
icon-text-fit-padding | supported | low | Per-side [top,right,bottom,left] padding on the fitted quad, dependent on icon-text-fit (#777 I-A). Converter emits per-side label-icon-text-fit-padding-{t,r,b,l}-N (only inside the fit branch; a zero side / default [0,0,0,0] emits nothing → byte-identical) → LabelDef.iconTextFitPadding → IconStage adds it (× dpr) to the fitted dims and shifts the quad centre for asymmetric padding. Negatives clamp to 0 with a warning. | — |
icon-keep-upright | supported | low | Flip line-placed icons so they face up (icon twin of text-keep-upright). Constant form lowered (#777 I-B): converter emits label-icon-keep-upright / label-icon-keep-upright-false → LabelDef.iconKeepUpright → label-pass dispatchIcon folds a downward segment tangent into the upright half-plane (resolveIconRotateRad) under symbol-placement=line + icon-rotation-alignment=map. DEFAULT: Mapbox's spec default is `true`, but X-GIS activates the fold ONLY on an EXPLICITLY authored value — an ABSENT property keeps today's always-follow-tangent render byte-identical (icon-allow-overlap absent-default precedent); flipping the absent-default requires its own §5 before/after sweep. Non-constant (zoom-interp / data-driven) forms warn and skip. | — |
icon-pitch-alignment | unsupported | low | viewport (default) / map / auto. X-GIS uses viewport-aligned icons unconditionally; map mode would project the icon quad onto the ground plane. | — |
Paint — background
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
background-color | supported | low | Constant + CSS form fold to a hex; interpolate-by-zoom resolves per frame (WS-1) — flat via the background-pass clear, sphere via the synthetic earth-surface show paintShapes.fill. | — |
background-opacity | supported | low | Constant numeric form folds into background-color hex alpha (iter 47, mirror of circle-stroke-opacity iter 4). Interpolate-by-zoom emits an opacity: style property that resolves per frame (WS-1) and multiplies into the background clear alpha on the FLAT path. On sphere/globe the synthetic earth-surface show carries the colour shape but the separate per-zoom opacity is not applied there (the earth surface is opaque; sphere bg-opacity semantics are a documented follow-up). | — |
background-pattern | supported | low | Constant sprite-name form lowers to a `pattern:` style property (#777 I-E); the background pass tiles the sprite over the coverage clear as its first draw — a fullscreen quad with analytic wrapped UV into the sprite atlas sub-rect, from a self-contained dual-source (WGSL+GLSL) DSL module. The zoom-crossfade form warns once and drops. | — |
Paint — fill
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
fill-color | supported | — | Constant + interpolate-by-zoom + per-feature case/match expressions. | paint.ts:91 |
fill-opacity | supported | — | — | paint.ts:133 |
fill-antialias | partial | low | Default `true` byte-identical (current render path). Geometric fill-edge AA in X-GIS comes from pipeline MSAA, not a per-fragment coverage smoothstep, so it is not per-layer disable-able. The `false` opt-out IS now wired: the converter emits a `fill-antialias-false` flag (paint.ts) → ShowCommand.fillAntialias → the polygon uniform's spare cam_ecef_off_h.w lane → the fs_fill fragment gates the only fill-alpha smoothstep it has (the sphere-rim hemisphere fade, polygon_rim_alpha) on the flag, giving a hard rim edge. On flat-Mercator the rim factor is already 1.0 so `false` is visually inert there; it bites on the curved-globe/azimuthal rim. OFM liberty `landcover_wood`/`grass`/`ice` set `false`. | paint.ts fill-antialias-false / polygon.ts buildFsFill rim gate |
fill-outline-color | supported | — | Lowers to `stroke-<color> stroke-1` on the same fill layer — the xgis polygon renderer paints fill + outline in the same pass. Constant + interpolate-by-zoom. | paint.ts:153 |
fill-pattern | supported | high | Stage 2 (true UV-tiled bitmap) landed iter-181/182/183 2026-05-20. Sprite atlas bound at @group(0) @binding(5) on every polygon pipeline + dedicated `sprite_samp` at binding(6). `fs_fill_pattern` fragment shader samples the atlas at world-anchored UV computed from `abs_merc / pattern_repeat_m`; pattern repeat in Mercator metres derived per-frame from sprite design CSS-px width × WORLD_MERC / (256 * 2^cameraZoom) so the bitmap stays anchored to the ground. Pattern parameters pack into reused uniform slots (fill_color = UV bbox, fill_translate = repeat metres) so the 192-byte Uniforms struct is unchanged. VTR routes fillPattern shows to `fillPipelinePatternGround` (+ Fallback) variant; ground polygons on the baseBindGroupLayout path only — variant + featureBindGroupLayout pattern shows fall through to the Stage 1 sprite-centre-pixel colour. Constant string form supported end-to-end. Documented trade-offs: pattern shows cannot also use solid fill-color or fill-translate; extrude-pattern walls still flat (Stage 2 ground-only). | paint.ts iter-177/181/182/183 |
fill-translate | supported | low | WS-1 — constant vec2 AND per-frame zoom-interp. The converter splits the Mapbox vec2 interpolate into scalar x/y bracket bindings (fill-translate-x-[interpolate(zoom,…)]); lower builds fillTranslate{X,Y}Shape; resolveShow resolves each frame (resolveNumberShape) into ResolvedShow.fillTranslateX/Y; VTR bakes CSS-px → NDC (`clip.xy += u.fill_translate * clip.w` in vs_main). Replaces the old last-stop approximation (iter 508). OFM building-top pseudo-3D roof offset honoured. | paint.ts:addFillTranslate + resolved-show.ts + vector-tile-renderer.ts |
fill-translate-anchor | supported | low | viewport (default) = screen-space offset, byte-identical to the historical path (emits nothing). map = world-space: the converter emits `fill-translate-anchor-map` (addTranslateAnchor) → lower sets RenderNode.fillTranslateAnchorMap → ShowCommand → VTR rotates the [dx,dy] offset by camera.bearing before the CSS-px → NDC bake, so the offset tracks the map world axes. Pitch foreshortening of a map-anchored offset is not reproduced by the clip-space bake (bearing rotation is the dominant/flat behaviour). Depends on fill-translate. | paint-fill.ts addTranslateAnchor + vector-tile-renderer.ts bearing rotate |
Paint — line
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
line-color | supported | — | — | paint.ts:102 |
line-width | supported | — | Constant + interpolate-by-zoom (linear AND exponential base) + per-feature width. PR #104 added per-frame zoom-stops; PR #108 conformance test pins differential parity with MapLibre createExpression() at z=4..20 (incl. fractional zooms). | paint.ts:113 |
line-opacity | supported | — | — | paint.ts:133 |
line-dasharray | supported | medium | WS-1 — constant numeric array AND per-frame zoom-interp. The converter emits a bracket binding (stroke-dasharray-[interpolate(zoom, z, [a,b], …)]); extractInterpolateZoomArrayStops lowers the array-valued stops to StrokeValue.dashArrayShape (PropertyShape<number[]>); resolveShow STEPs to the nearest zoom stop (resolveArrayShape — Mapbox line-dasharray is interpolated:false) into ResolvedShow.dashArray; VTR prefers it over the static array, scaling by mpp. data-driven (per-feature) dash still drops with a warning. | paint.ts:addStrokeDash + lower-helpers.ts:extractInterpolateZoomArrayStops + paint-shape-resolve.ts:resolveArrayShape |
line-blur | supported | — | Edge feathering in CSS px. The line shader uses `aa_width_px` to widen both the geometry quad and the smoothstep range so the edge soft-fades over `1.5 + blur` px each side. Constant only — interpolate-by-zoom warns and drops. | paint.ts:190 |
line-gap-width | supported | medium | Constant + zoom-interp last-stop approx end-to-end via stroke-gap-N utility. Runtime double-draws each line at ±(gap+stroke)/2 via writeLayerSlot (iter 499). OFM road-casing layers honoured. Iter 498 + 499 + 513 shipped 2026-05-18. | paint.ts:addLineGapWidth |
line-offset | supported | — | Positive Mapbox values (right of travel) → `stroke-offset-right-N`; negative → `stroke-offset-left-N`. The xgis line renderer threads `strokeOffset` through to the vertex shader including offset-aware miter / join geometry. Constant only — interpolate-by-zoom warns and drops. | paint.ts:175 |
line-translate | supported | low | WS-1 — constant vec2 AND per-frame zoom-interp (mirrors fill-translate). Converter emits scalar stroke-translate-{x,y} bracket bindings for the zoom-interp form; lower builds strokeTranslate{X,Y}Shape; resolveShow resolves each frame into ResolvedShow.strokeTranslateX/Y; VTR bakes CSS px → NDC into LineLayer uniform slots 48/49 (u.line_translate_x/y), applied in vs_line post-MVP. Anchor (viewport / map) handled by line-translate-anchor. | paint.ts:addLineTranslate + resolved-show.ts |
line-translate-anchor | supported | low | viewport (default) = screen-space offset, byte-identical to the historical path (emits nothing). map = world-space: the converter emits `stroke-translate-anchor-map` (addTranslateAnchor; the line translate rides the stroke-translate namespace) → lower sets RenderNode.strokeTranslateAnchorMap → ShowCommand → VTR rotates the [dx,dy] offset by camera.bearing before the CSS-px → NDC bake. Pitch foreshortening of a map-anchored offset is not reproduced by the clip-space bake (bearing rotation is the dominant/flat behaviour). Depends on line-translate. | paint-line.ts addTranslateAnchor + vector-tile-renderer.ts bearing rotate |
line-pattern | supported | low | Stage 2 landed iter-185 2026-05-20. line-renderer declares sprite_atlas at binding 5 + sprite_samp at binding 6 (shared TileBindGroupLayout with VTR so iter-181/182 atlas binding is already attached). New `fs_line_pattern` fragment + `pipelinePattern` alpha-blend pipeline. Pattern shows route via getDrawPipeline(translucent, patternActive=true). World-anchored UV (abs_merc / repeat_m) — Stage 2.1 along-line UV (arc length + transverse v) is a follow-up refinement. UV bbox packed into stroke_color uniform slot (20-23); repeat metres packed into layer.color.r / .a via writeLayerSlot override. Constant string form supported end-to-end. iter-165 probe: ZERO line-pattern uses in OFM bright/liberty target fixtures, so visual A/B unavailable against current set — Stage 2 is insurance for other styles (USA OSM / custom sprites). | line-renderer.ts iter-178/185 |
line-gradient | unsupported | low | Gradient along the line via ["line-progress"]. iter-166 probe: ZERO uses in OFM bright/liberty (also 0 lineMetrics declarations) — empirically confirms the low impact rating. Implementation cost (iter-158 scoping, the renderer change is NOT the hard part): (1) PREREQUISITE — geojson-vt currently IGNORES source.lineMetrics (geojsonvt/index.ts:14, sources.ts:406). line-progress is normalised over the ORIGINAL feature but geojson-vt clips lines per tile, so the clip stage must track each clipped segment's [progressStart,progressEnd] fraction of the original arc-length. This compiler-tiler change is the bulk of the work. (2) line-segment-build.ts interpolates per-vertex progress 0..1. (3) new per-vertex progress attribute + WGSL line fragment samples a gradient LUT the converter emits from the line-gradient interpolate stops. ~5 files; multi-day; not a surgical fix. PMTiles vector sources can't support it anyway (don't preserve original-line arc-length across tile boundaries) — feature is GeoJSON-source-with-lineMetrics-true only, niche. | paint.ts:218 specific warning |
Paint — symbol
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
text-color | supported | — | Constant + interpolate-by-zoom + per-feature colorExpr. | layers.ts:199 |
text-opacity | supported | — | Constant folded into label-color alpha (applyAlphaMultiplier). Zoom-interp + data-driven emit `label-opacity-[…]` → LabelShapes.opacity PropertyShape; runtime resolveNumberShape multiplies into resolvedColor.a + resolvedHalo.color.a per frame. Iter 113. | layers.ts:480 |
text-halo-color | supported | — | Constant + interpolate-by-zoom. | layers.ts:269 |
text-halo-width | supported | — | Constant + interpolate-by-zoom; PR #76 fixed scaling into SDF units. | layers.ts:259 |
text-halo-blur | supported | — | Constant only at conversion; IR exposes a PropertyShape so future zoom-interp / data-driven emit lands without IR changes. | layers.ts:283 |
text-translate | supported | — | Pixel-space offset added on top of em-unit text-offset. | layers.ts:340 |
text-translate-anchor | supported | — | viewport (default) = screen-space (byte-identical historical path); map = world-space: the converter emits `label-translate-anchor-map` (layers-symbol.ts text-translate-anchor) → LabelDef.translateAnchorMap → TextStage.prepare rotates the [dx,dy] text-translate by the map bearing (rotateLabelTranslate, mirror of the fill/line clip-space bake — Phase S Batch 2) before the per-anchor pixel add; the rotated value also re-keys the layout cache so a bearing change never serves a stale offset. Pitch foreshortening of the offset not reproduced. | layers-symbol.ts text-translate-anchor / text-stage.ts rotateLabelTranslate |
icon-color | supported | — | SDF sprite tint. iter 138 (Plan §4): IconRenderer carries a per-vertex tint + fwidth SDF fragment path; one batch mixes raster + SDF quads (per-vertex sdf flag, no pipeline split). Constant + zoom-interp + data-driven all route through LabelShapes.iconColor PropertyShape<RGBA> (same contract as text-color); runtime resolveColorShape at dispatchIcon → IconStage tint. Raster sprites ignore the tint per Mapbox spec. | layers.ts icon-color emit / icon-renderer.ts fs sdf branch |
icon-opacity | supported | — | Constant + zoom-interp + data-driven all route through LabelShapes.iconOpacity PropertyShape. Runtime resolveNumberShape at dispatchIcon → IconStage.addIcon per-vertex alpha. Iter 113. | layers.ts:1260 |
icon-halo-color | n/a | — | na — SDF-sprite-only; no SDF sprite in scope. icon-halo applies ONLY to SDF sprites (Mapbox spec); the sprite loader DOES read the flag (sprite-atlas-host.ts parseMetadata, `sdf: e.sdf === true`) but no in-scope sprite sets it: the iter-162 probe (playground/scripts/sprite-sdf-buffer-probe.ts) fetched the live OFM bright sprite — 264 entries, ZERO SDF — and the committed fixture-sprite.json declares none either, so this property is a guaranteed NO-OP on every target style. Reclassified unsupported → na (#777 I-H): a real gap only appears once an SDF sprite source becomes a target. Revisit then — the iter-138 SDF icon foundation (icon.ts fragment branch + per-vertex tint) already serves SDF icons; the implementation recipe is a second smoothstep at edge-haloWidth mirroring fs_text, per-vertex halo attrs extending the 9-float format (icon-renderer.ts / icon-vertex-format.ts), and a converter paint emit — the spritezero buffer constant stays UNRESOLVED until the probe pins it against a real SDF sprite. | — |
icon-halo-width | n/a | — | na — SDF-sprite-only; no SDF sprite in scope. Same disposition as icon-halo-color (iter-162 probe: OFM bright 0 SDF icons, fixture-sprite.json 0 SDF → guaranteed no-op). Reclassified unsupported → na (#777 I-H); revisit if an SDF sprite source lands. | — |
icon-halo-blur | n/a | — | na — SDF-sprite-only; no SDF sprite in scope. Same disposition as icon-halo-color (iter-162 probe: OFM bright 0 SDF icons, fixture-sprite.json 0 SDF → guaranteed no-op). Reclassified unsupported → na (#777 I-H); revisit if an SDF sprite source lands. | — |
icon-translate | partial | low | CSS-px viewport offset for icons (independent of text-translate). Constant [dx, dy] form wired end-to-end: converter emits `label-icon-translate-{x,y}-N` (layers-symbol.ts) → LabelDef.iconTranslateX/Y → dispatchIcon adds it (× dpr) to the icon anchor before IconStage.addIcon (label-pass.ts), alongside icon-offset. Default [0,0] = no-op. #777 I-F: the per-feature EXPRESSION form (case/match/get → [dx,dy]) now lowers to `label-icon-translate-[<expr>]` → LabelDef.iconTranslateExpr → applyFeatureExprs evaluates it per feature into iconTranslateX/Y. Still PARTIAL for one residual sub-form: zoom-`interpolate` of the [dx,dy] tuple snaps to the nearest stop (the runtime evaluate does NOT component-interpolate array-valued stops), so a smoothly zoom-animated translate is approximate. | layers-symbol.ts icon-translate emit / label-pass.ts dispatchIcon |
icon-translate-anchor | supported | — | viewport (default) = screen-space (byte-identical); map = world-space: the converter emits `label-icon-translate-anchor-map` (layers-symbol.ts icon-translate-anchor) → LabelDef.iconTranslateAnchorMap → dispatchIcon rotates ONLY the icon-translate portion of the icon anchor offset by the map bearing (icon-offset, a layout nudge, stays screen-space) before IconStage.addIcon — mirror of text-translate-anchor / fill/line Phase S Batch 2. Pitch foreshortening of the offset not reproduced. | layers-symbol.ts icon-translate-anchor / label-pass.ts dispatchIcon |
Paint — circle
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
circle-radius | supported | — | Constant + interpolate-by-zoom + per-feature expression. CSS px (Mapbox radius = xgis size). | layers.ts:537 |
circle-color | supported | — | Constant + interpolate-by-zoom + per-feature case/match. | — |
circle-opacity | supported | — | Mapbox 0..1 → xgis 0..100 scaled. Constant + interpolate-by-zoom. | — |
circle-stroke-color | supported | — | — | — |
circle-stroke-width | supported | — | CSS px; constant + interpolate-by-zoom. | — |
circle-blur | partial | low | Constant numeric form extends the point fragment smoothstep AA band via circle_params.z in the point uniform (layers-circle.ts). Zoom-interp / data-driven forms warn + drop — need a per-feature feat_data slot for per-feature blur. | layers-circle.ts:circle-blur block |
circle-stroke-opacity | supported | low | Constant numeric form folds into stroke-color hex alpha at compile time (iter 4). Zoom-interp form (WS-1, part 4) emits a stroke-opacity-[interpolate(zoom, …)] binding that lower.ts threads to ShowCommand.circleStrokeOpacityShape; PointRenderer.updateDynamicSizes resolves it per frame (resolveNumberShape) and multiplies the alpha into the circle's baked stroke alpha (feat_data slot 8). Non-interpolate data-driven forms still warn + drop. | layers-circle.ts:circle-stroke block |
circle-translate | supported | low | Constant [dx, dy] vec2 AND per-frame zoom-interp now wired end-to-end through the point frame uniform (circle_params.xy — uf 32/33). The constant form emits circle-translate-x-N / circle-translate-y-M; the zoom-interp form splits the vec2 per-axis into circle-translate-{x,y}-[interpolate(zoom, …)] bindings (mirrors addFillTranslate). lower.ts threads both the constant ShowCommand.circleTranslateX/Y and the circleTranslate{X,Y}Shape; PointRenderer.updateDynamicSizes resolves the shapes each frame (resolveNumberShape) into the layer translate the uniform bakes to NDC-per-pixel. This also closed the prior gap where the GeoJSON point addLayer path (map.ts) never threaded circle-translate at all. circle-translate-anchor:map stays deferred (WS-4a). | layers-circle.ts:circle-translate block |
circle-translate-anchor | partial | low | viewport (spec default) is the only honoured mode — X-GIS point renderer always applies the translate in viewport/NDC space. 'map'-anchor (world-space shift) is unsupported and warns + drops. The anchor no-op suppression (when circle-translate is absent) mirrors fill-translate-anchor behaviour. | layers-circle.ts:circle-translate-anchor block |
circle-pitch-scale | supported | low | viewport (spec default — radius constant in screen px, byte-identical) AND map. The converter emits a circle-pitch-scale-map flag only for 'map'; lower threads it through RenderNode/ShowCommand.circlePitchScaleMap → PointRenderer packs it into the point uniform circle_params.w, and the point VS scales the screen radius by w_ref/clip.w (w_ref = mvp[3][3]) so circles foreshorten with pitch/distance — mirrors MapLibre circle.vertex.glsl's pitch-scale:map for the viewport-aligned path. | layers-circle.ts:circle-pitch-scale block |
circle-pitch-alignment | unsupported | low | viewport (spec default — billboard, byte-identical) vs map. X-GIS uses viewport-aligned (camera-facing) circles; 'map' mode (disc lies flat on the ground plane, tilts/foreshortens with pitch) needs a per-projection ground-tangent quad expansion across the flat-Merc / ECEF / globe vertex spaces and is deferred. 'map' still warns + drops. | — |
Paint — fill-extrusion
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
fill-extrusion-color | supported | — | — | — |
fill-extrusion-opacity | supported | — | — | — |
fill-extrusion-height | supported | — | Constant + interpolate-by-zoom + per-feature expression. | paint.ts:154 |
fill-extrusion-base | supported | — | — | paint.ts:165 |
fill-extrusion-translate | supported | low | WS-1 — routed through addFillTranslate alongside fill-translate, so it inherits the per-frame zoom-interp path (fillTranslate{X,Y}Shape → resolveShow → VTR). The fill-extrusion vertex shaders (vs_main_quantized + vs_main_quantized_extruded) apply u.fill_translate_x/y. Constant vec2 AND per-frame zoom-interp supported. Replaces the old last-stop approximation (iter-180). | paint.ts:addFillTranslate + resolved-show.ts |
fill-extrusion-translate-anchor | supported | low | viewport (default) = screen-space, byte-identical (emits nothing). map = world-space: fill-extrusion-translate rides the SAME fill-translate-{x,y} utilities + slot 46/47 uniform as fill (the extrude vertex shaders apply u.fill_translate_x/y), so the converter emits `fill-translate-anchor-map` (addTranslateAnchor, fill prefix) → RenderNode.fillTranslateAnchorMap → ShowCommand → VTR rotates the [dx,dy] offset by camera.bearing before the px→NDC bake — the extrude path inherits the rotation for free. Pitch foreshortening not reproduced. Depends on fill-extrusion-translate. | paint-fill-extrusion.ts addTranslateAnchor + vector-tile-renderer.ts bearing rotate |
fill-extrusion-pattern | supported | low | Stage 2 landed iter-186 2026-05-20. New `fillPipelinePatternExtruded` + Fallback variants (vs_main_quantized_extruded vertex + extrudedZBufferLayout for per-feature z + fs_fill_pattern fragment). VTR routes extruded pattern shows via setPatternExtrudedPipelines + an extrudedPatternActive gate symmetric with the iter-183 ground path. Same world-anchored UV math as fill-pattern + line-pattern (abs_merc / repeat_m). Documented Stage 2 trade-off: pattern-extrude shows lose the per-fragment wall_shade lighting — sprite colour replaces the shaded fill rgb directly. Stage 2.1 (dedicated fs_fill_pattern_extruded that multiplies the sample by wall_shade) is a follow-up refinement. Constant string form supported end-to-end. iter-165 probe: ZERO uses in OFM bright/liberty target fixtures — Stage 2 is insurance for other styles. | paint.ts:270 iter-179/186 |
fill-extrusion-vertical-gradient | supported | low | Default `true` is honoured end-to-end — the extrude vertex shader applies the 0.7→1.0 vertical-gradient wall ramp matching MapLibre. The `false` opt-out is now wired: converter emits `fill-extrusion-vertical-gradient-false` (paint.ts) → ShowCommand.fillExtrusionVerticalGradient → the polygon uniform's spare cam_ecef_off_l.w lane → vs_main_ecef_extruded ANDs the flag into the per-wall gradient test so walls shade flat. Default path is byte-identical (flag = 1). | paint.ts fill-extrusion-vertical-gradient-false / polygon.ts vs_main_ecef_extruded vgrad gate |
fill-extrusion-ambient-occlusion-intensity | unsupported | low | AO would need per-vertex normal + screen-space AO pass. Not in current renderer. | — |
fill-extrusion-ambient-occlusion-radius | unsupported | low | See fill-extrusion-ambient-occlusion-intensity. | — |
Paint — raster
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
raster-opacity | supported | — | Constant + interpolate-by-zoom + data-driven (all PropertyShape kinds) routed through the global RasterRenderer opacity uniform. Single raster show per scene is supported; multi-raster styles fall back to the first declared show. | paint.ts:38 |
raster-hue-rotate | supported | — | Constant degrees. Rotates the sampled texel hue (MapLibre spinWeights) in the raster fragment shader. Default 0 is a no-op. | paint-raster.ts |
raster-brightness-min | supported | — | Constant 0..1. Lower bound of the fragment brightness remap (mix(min,max,rgb)). Default 0 is a no-op. | paint-raster.ts |
raster-brightness-max | supported | — | Constant 0..1. Upper bound of the fragment brightness remap. Default 1 is a no-op. | paint-raster.ts |
raster-saturation | supported | — | Constant -1..1. HSL saturation multiplier applied in the raster fragment shader (MapLibre saturation factor). Default 0 is a no-op. | paint-raster.ts |
raster-contrast | supported | — | Constant -1..1. Fragment contrast scale (MapLibre contrast factor about 0.5). Default 0 is a no-op. | paint-raster.ts |
raster-fade-duration | unsupported | low | Crossfade between zoom levels. X-GIS swaps tiles atomically; no fade. | — |
raster-resampling | supported | — | linear (default) vs nearest. nearest selects a nearest-filtered GPUSampler (pixel-art / DEM staircase). Default linear is byte-identical to the historical fixed-linear sampler. | paint-raster.ts |
resampling | supported | — | MapLibre v3 alias for raster-resampling — same value space + same nearest-sampler path. raster-resampling wins if both are present. | paint-raster.ts |
Paint — heatmap
Heatmap layer renderer is not implemented; every property here is unsupported pending a roadmap entry.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
heatmap-radius | supported | — | Per-feature Gaussian splat radius (CSS px). Constant fully; interpolate-by-zoom resolved to the deepest-zoom stop value (full per-frame zoom-interp deferred). GeoJSON-source points. | layers-heatmap.ts |
heatmap-weight | supported | — | Per-feature contribution multiplier (constant + data-driven). Default 1. | layers-heatmap.ts |
heatmap-intensity | supported | — | Overall density scale. Constant fully; interpolate-by-zoom resolved to the deepest-zoom stop value. | layers-heatmap.ts |
heatmap-color | partial | medium | Density → colour ramp. The runtime applies its default Mapbox ramp; a custom `interpolate` over `heatmap-density` is not yet baked into the LUT (converter warns). | layers-heatmap.ts |
heatmap-opacity | supported | — | Layer-level opacity 0..1. Constant fully; interpolate-by-zoom resolved to the deepest-zoom stop value. | layers-heatmap.ts |
Paint — hillshade
Hillshade layer renderer is not implemented; raster-dem source is recognised but produces no output.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
hillshade-illumination-direction | partial | medium | Light azimuth (numberArray, deg from N). Single-source constant lowered → shader azimuth; a multidirectional (>1) array warns + uses element 0. | — |
hillshade-illumination-altitude | partial | medium | Light elevation (numberArray, 0–90°). Single-source constant lowered (used by the basic model); multidirectional array warns + uses element 0. | — |
hillshade-illumination-anchor | partial | low | map / viewport — viewport (default) folds the camera bearing into the light azimuth per frame; map anchors the light to data space. Constant only. | — |
hillshade-exaggeration | partial | medium | Vertical-relief multiplier (constant). Lowered → shader intensity; non-constant (zoom-interp / data-driven) forms warn + drop. | — |
hillshade-shadow-color | partial | medium | Shadow-side colour (colorArray). Single-source constant lowered (premultiplied); a multi-source colour array warns + uses element 0. | — |
hillshade-highlight-color | partial | medium | Lit-side colour (colorArray). Single-source constant lowered (premultiplied); a multi-source colour array warns + uses element 0. | — |
hillshade-accent-color | partial | low | Accent tint (single colour) — used by the standard model. Constant lowered (premultiplied); non-constant warns + drops. | — |
hillshade-method | partial | low | standard (default) + basic (GDAL-Lambert) implemented in fs_hillshade; combined / igor / multidirectional warn and approximate via the basic model. | — |
resampling | partial | low | linear (spec default) / nearest DEM sampling. The MVP single-pass fragment renders the nearest 3×3 field (byte-parity with MapLibre nearest); linear decoded-height smoothing is the documented two-pass upgrade. | — |
Expression operators
Mapbox Style Spec v1 expression form (the bracketed `["op", …]` syntax).
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
literal | supported | — | Scalar + array forms. Null-valued wrappers (`["literal", null]`) treated as "property omitted" by the paint-helper gate (isOmitted in paint.ts). | expressions.ts:33 |
get | supported | — | Bare field for identifier-safe names; `get("name:xx")` for colon-bearing locale keys. | expressions.ts:25 |
has | supported | — | — | expressions.ts:43 |
!has | supported | — | — | expressions.ts:52 |
coalesce | supported | — | Lowers to xgis `??` chain. | expressions.ts:59 |
case | supported | — | — | expressions.ts:65 |
match | supported | — | Routes through `match() { … }` when input is FieldAccess; ternary fallback otherwise. | expressions.ts:83 |
step | supported | — | — | expressions.ts:185 |
let / var | supported | — | Pure substitution at convert time. | expressions.ts:199 |
all | supported | — | — | — |
any | supported | — | — | — |
none | supported | — | Legacy filter combinator. Lowers to `!(f1 || f2 || …)`. | — |
! | supported | — | — | — |
== / != / < / <= / > / >= | supported | — | — | — |
in | supported | — | Both expression form and legacy form. Empty value list lowers to constant `false` per spec. | expressions.ts:560 |
!in | supported | — | — | — |
+ / - / * / / / % | supported | — | — | — |
min / max | supported | — | — | — |
^ / abs / ceil / floor / round / sqrt | supported | — | — | — |
sin / cos / tan / asin / acos / atan | supported | — | — | — |
ln / log10 / log2 | supported | — | — | — |
pi / e / ln2 | supported | — | Zero-arg constants. | — |
concat | supported | — | — | — |
length | supported | — | — | — |
upcase / downcase | supported | — | — | — |
at | supported | — | Array indexing. | — |
to-number / number | supported | — | Converter passes through to a coalesce chain; xgis evaluator coerces in arithmetic context. Iter 539 added spec-compliant `to_number(v, fallback…)` builtin in the evaluator for hand-authored xgis source / tooling chains that bypass the converter. | evaluator.ts:to_number |
to-string / to-boolean / to-color | supported | — | Converter passes through to coalesce chains; iter 539 added spec-compliant `to_string` / `to_boolean` builtins in the evaluator (null → "", number → str, etc.); iter 541 added `to_color` (hex regex validation, X-GIS hex-only — converter pre-resolves CSS names like "red" via tokens/colors.ts:resolveColor). | evaluator.ts:to_string + to_boolean + to_color |
rgb / rgba | supported | — | Constant channels hex-encode at convert time; per-feature (data-driven) channels — `["rgb", ["get","r"], …]` — now lower to a runtime `rgb(…)` / `rgba(…)` call (expr-string.ts rgbHandler) that the evaluator's rgb/rgba builtin resolves per feature into a hex string (encoding byte-matches the constant path). Classified per-feature-CPU (rgb ∉ GPU_SAFE_BUILTINS). Per-channel v8 literal-wrap (`["literal", N]`) accepted. | expr-string.ts rgbHandler + eval/evaluator-helpers.ts callBuiltin rgb/rgba |
hsl / hsla | supported | — | Constant channels convert via CSS hsl()/hsla() and re-hex at convert time; per-feature channels now lower to a runtime `hsl(…)` / `hsla(…)` call (expr-string.ts hslHandler) that the evaluator's hsl/hsla builtin resolves per feature through the CSS colour parser (tokens/colors.ts). Per-channel v8 literal-wrap accepted. | expr-string.ts hslHandler + eval/evaluator-helpers.ts callBuiltin hsl/hsla |
interpolate (linear) | supported | — | — | — |
interpolate (exponential) | supported | — | Mapbox `["exponential", N]` lowers to `interpolate_exp(zoom, N, …)`; runtime applies the Mapbox curve formula. base=1 collapses to the linear fast path. | paint.ts:46 |
interpolate (cubic-bezier) | partial | low | Numeric-valued AND hex-colour-valued zoom/data-driven interpolates densify at compile time into a piecewise-linear approximation (6 samples per segment, CSS bezier-eased via Newton-Raphson; colour stops sampled in sRGB at the eased fraction). Runtime sees a longer linear stop list and visually approximates the bezier curve. Expression-valued (non-literal) stops still warn and fold to pure linear — eased samples can't be computed at compile time. Iter 60-62 + colour-stop landing. | expr-interpolate.ts (isBezier densify) + paint.ts:cssBezierEase |
interpolate-hcl | supported | — | LCh (polar Lab, hue shortest-path) colour interpolation: hex stops densify at compile time (iter 61-62 linear, iter 137 exponential — 6 samples / segment); non-hex (data-driven) stops now route to the runtime evaluator case interpolate_hcl (iter 164) which parses each stop's y at eval time, interpolates in LCh, and returns a hex. Full coverage modulo exponential×non-hex (rare combination — still warns and downgrades). | paint.ts + expressions.ts + eval/evaluator.ts interpolate_hcl |
interpolate-lab | supported | — | Lab (D50) colour interpolation: hex stops densify at compile time (iter 61-62 linear, iter 137 exponential — 6 samples / segment); non-hex (data-driven) stops now route to the runtime evaluator case interpolate_lab (iter 164) which parses each stop's y at eval time, interpolates in Lab, and returns a hex. Full coverage modulo exponential×non-hex (rare combination — still warns and downgrades). | paint.ts + expressions.ts + eval/evaluator.ts interpolate_lab |
geometry-type | supported | — | Routes via synthetic `$geometryType` prop injected at filter-eval time. | expressions.ts:263 |
id | supported | — | Routes via synthetic `$featureId` prop injected from `feature.id` (GeoJSON RFC 7946 §3.2; MVT feature.id) at every filter-eval site. Same pattern as `geometry-type`. | expressions.ts:278 |
properties | supported | — | Mapbox `["properties"]` lowers to a `properties()` builtin (mirror of the `geometry-type` / `id` accessor pattern). The evaluator special-cases it (eval/evaluator.ts evaluateFnCall) — it holds the live props bag and returns a shallow copy of feature.properties with the reserved $-sigil keys ($zoom / $pitch / $featureId / $geometryType) stripped, matching Mapbox's "the feature's own properties" semantic. Useful as the whole-object operand to a downstream comparison; per-field access still prefers `.field` / `get("field")`. | expr-lookup.ts propertiesHandler |
feature-state | n/a | — | Mapbox v8 dynamic property setter — no xgis equivalent. | — |
typeof | supported | — | Returns Mapbox-shaped strings ("string" / "number" / "boolean" / "object" / "null"). | expressions.ts:237 |
format | partial | low | Span texts concatenated via xgis concat(); per-span TYPOGRAPHY opts (font-scale / text-color / text-font / vertical-align) dropped — X-GIS labels render with one style per layer. Per-section partial-drop semantics: when one section fails to convert (e.g. uses an unsupported accessor), surviving sections still concat — only ALL-sections-fail returns null. An `["image", …]` section IS carried now (#777 I-G): it lowers to the `image(name)` builtin → an inline sprite quad on the text baseline (imageHandler + evaluator; see the `image` row). Still partial only for the typography opts. | expressions.ts:208 |
image | supported | — | Resolved in BOTH contexts. icon-image PROPERTY (#777 I2): the converter strips the `["image", …]` wrapper (recursively, incl. nested inside the coalesce/match arms of a data-driven icon-image) and lowers the inner sprite-name — constant `["image","airport"]` → LabelDef.iconImage "airport"; data-driven → per-feature LabelDef.iconImageExpr → IconStage.addIcon. TEXT/format inline image (#777 I-G): the bare `text-field: ["image","pat"]` form AND an `["image", …]` section inside `["format", …]` lower to the `image(name)` builtin (imageHandler) whose evaluator wraps the resolved name in PUA sentinels; the runtime label shaper (TextStage) reserves the sprite CSS-width advance and hands the placement to IconStage → an inline sprite quad on the text baseline. Missing sprite → the image is skipped, surrounding text keeps rendering (MapLibre parity). | expr-string.ts imageHandler + evaluator-helpers.ts case image + layers-helpers.ts unwrapImageExpr |
number-format | supported | — | Lowers to positional `number_format(input, minFrac, maxFrac, locale, currency)` (xgis has no object-literal syntax). Routes through Intl.NumberFormat at runtime; null slots use spec defaults. | expressions.ts:275 |
collator | partial | low | Locale-aware comparator as the trailing 4th arg of ==/!=/</<=/>/>=. Constant collator options (case-sensitive / diacritic-sensitive / locale) are fully supported: comparisonHandler lowers `["==", a, b, ["collator", opts]]` to the `collator_cmp` CPU builtin (eval/collator.ts) backed by Intl.Collator. Non-constant (per-feature expression) options fall back to byte-exact compare with a warning; a STANDALONE `["collator", …]` (not on a comparison) still warns (no value alone). | — |
resolved-locale | partial | low | Returns the BCP-47 tag a collator resolves to. Constant collator locale supported: resolvedLocaleHandler lowers `["resolved-locale", ["collator", opts]]` to the `resolved_locale` CPU builtin (Intl.Collator.resolvedOptions().locale). Non-constant collator options warn + drop. | — |
is-supported-script | supported | — | Mapbox `["is-supported-script", str]` returns true when every char is shapeable. X-GIS rasterises through Canvas2D / the MapLibre PBF atlas with a CJK + Latin + Arabic fallback chain and makes no per-script capability distinction — it treats all Unicode as renderable. The converter lowers the accessor to the constant `true` identifier (isSupportedScriptHandler in expr-lookup); styles that gate a label via `["case", ["is-supported-script", text], text, <fallback>]` always take the supported branch, matching X-GIS' actual capability. | expr-lookup.ts isSupportedScriptHandler |
array | partial | low | Type-assertion drops to value pass-through (X-GIS arrays carry no per-element type tag, so the spec's "abort if not array" semantic is lost; in paint/filter use a non-array would null-cascade anyway). | expressions.ts:163 |
slice | supported | — | String or array; Mapbox `["slice", input, start[, end]]`. Routes to JS String/Array `.slice` semantics. | expressions.ts:248 |
index-of | supported | — | Lowers to xgis `index_of(needle, haystack[, from])`. Returns -1 when not found. | expressions.ts:257 |
zoom | supported | — | Lowers to bare `zoom` identifier. Works in `interpolate(zoom, …)` / `step(zoom, …)` AND anywhere else (filter compare, case condition, arithmetic). | expressions.ts:471 |
pitch | supported | low | Mapbox `["pitch"]` lowers to a bare `pitch` identifier (mirror of the `zoom` path). The evaluator resolves it via the reserved `$pitch` key (CAMERA_PITCH_KEY), injected by the render-path eval sites (map.ts applyFilter + per-feature paint/size eval, feature-helpers applyFilter/applyGeometry) from `camera.pitch` (degrees). Decode-time/worker sites have no camera so `["pitch"]` resolves to null there — same proxy contract `["zoom"]` has with tileZoom. | expressions.ts case pitch / eval/evaluator.ts + reserved-keys.ts |
distance-from-center | unsupported | low | Returns the current feature's screen-space distance from the viewport centre (globe-mode horizon fade). The converter RECOGNISES the op and emits a precise warning (expressions.ts KNOWN_UNSUPPORTED) so it never falls to the generic catch-all — but it returns null, NOT a value: X-GIS has no per-feature/per-frame camera-relative evaluation hook in the expression model (feature exprs evaluate against the property bag only; there is no live camera-centre distance), so it cannot be implemented faithfully without a new per-frame feature-distance pass. Warned, not supported. | — |
distance | partial | low | Shortest distance (metres) between the feature and a target GeoJSON. Point/MultiPoint feature-geometry vs any target is supported on GeoJSON sources: distanceHandler decomposes the constant target into points/segments/polygons (compile time) and lowers to the `distance(get("$geometry"), …)` CPU builtin (eval/distance.ts), which uses a cheap-ruler (latitude-corrected planar) metre metric and treats inside-polygon as 0. Deferred (return null): LineString/Polygon feature-geometry and MVT/PMTiles tile-coordinate sources (the worker filter path does not inject `$geometry`). | — |
within | partial | low | Geometry-containment filter. Point/MultiPoint tested-geometry vs Polygon/MultiPolygon argument is fully supported on GeoJSON sources: the converter lowers ["within", poly] to `within(get("$geometry"), <coords>)` (expr-lookup.ts withinHandler) and applyFilter injects the `$geometry` reserved key; the CPU even-odd containment test (eval/within.ts) honours holes + MultiPolygon. Deferred (return false): LineString/Polygon tested-geometry (needs segment-vs-ring intersection) and MVT/PMTiles tile-coordinate sources (the worker filter path does not inject `$geometry`, and the polygon arg would need lng/lat→tile reprojection). | — |
accumulated | n/a | — | Heatmap-only. | — |
heatmap-density | n/a | — | Heatmap-only. | — |
line-progress | n/a | — | line-gradient only. | — |
sky-radial-progress | n/a | — | — | — |
Filters
Legacy + expression form. Most filter operators reuse the expression infrastructure.
| Property | Status | Impact | Note | Source |
|---|---|---|---|---|
== / != / < / <= / > / >= (legacy form) | supported | — | Field-as-second-arg shape recognised. | expressions.ts:420 |
in / !in (legacy + expression form) | supported | — | — | — |
has / !has | supported | — | — | — |
all / any / ! | supported | — | — | — |
match (boolean form) | supported | — | Lowers to OR/AND chain when all values are boolean literals. | expressions.ts:335 |
$type | supported | — | Legacy filter — routes to geometry-type accessor (get("$geometryType")). | expressions.ts |
$id | supported | — | Legacy filter — routes to id accessor (get("$featureId")). | expressions.ts |
Keeping this page accurate
The table lives in
compiler/src/convert/spec-coverage.ts.
A vitest regression
(spec-coverage-drift.test.ts)
scans the converter source files at build time and fails when:
- • A new
case 'X':,layout['X'], orpaint['X']reference appears in the converter without a corresponding table entry, or - • An entry marked
supportedhas no matching reference in the source (catches stale entries after the converter loses a feature).
Problem on this page?