X-GIS

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

Supported
184
81% of 228 applicable
Partial
27
Lossy or runtime gap
Unsupported
17
Dropped on convert
N/A
15
No xgis equivalent

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.

Showing 243 of 243

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'], or paint['X'] reference appears in the converter without a corresponding table entry, or
  • • An entry marked supported has no matching reference in the source (catches stale entries after the converter loses a feature).