Skip to content

feat(rust): [PoC] Support 3D geometry (GeometryZ)#1507

Draft
yutannihilation wants to merge 5 commits into
maplibre:mainfrom
yutannihilation:feat/rust-3d
Draft

feat(rust): [PoC] Support 3D geometry (GeometryZ)#1507
yutannihilation wants to merge 5 commits into
maplibre:mainfrom
yutannihilation:feat/rust-3d

Conversation

@yutannihilation

Copy link
Copy Markdown
Contributor

The current specification supports 3D geometry:

The x, y, and optional z coordinates are stored interleaved in a VertexBuffer for efficient CPU processing and direct copying to GPU buffers. If the z coordinate is not needed for rendering, it can be stored separately as an M-value (see vertex-scoped properties).

However, as far as I can tell, none of the current implementations support 3D geometry in actual. I know there's an ongoing discussion about 3D support, but I believe the MLT v1 spec is ready for supporting simple (x, y, z) coordinate.

This pull request is the proof of concept; while this is naively done by AI, I believe the implementation would be like this anyway if I implement all by hand. The changes are a bit lengthy, but the points are simple:

  • Use the geo-traits crate for 3D coordinates (as you know, the geo-types only supprts 2D)
    • As geo-traits doesn't provide an actual struct for 3D geometry, use the wkt crate for this.
  • Add GeometryZ to the column type, and treat Geometry and GeometryZ differently.

I also have the corresponding TypeScript implementation, and it works fine. So, I don't see any blocking issue on supporting 3D coordinates in this way.

Are there any particular reasons why the current implementations don't support the optional z coordinate? I don't expect this pull request to be merged as-is, but I'm hoping it can serve as a starting point for discussion — I'd love to know whether this is heading in the right direction!

yutannihilation and others added 4 commits June 19, 2026 18:21
Replace the 2D-only geo-types concrete types with the geo-traits
abstraction, laying the groundwork for experimental 3D (Z) support
without changing the wire format or any behavior.

- Encoder input now accepts any `&impl geo_traits::GeometryTrait<T = i32>`
  (so geo_types, wkt, or any geo-traits geometry works as input).
- Decoder output is now `wkt::Wkt<i32>` (dimension-aware, Z-capable),
  replacing `geo_types::Geometry<i32>` in TileFeature/FeatureRef/
  to_geojson/geojson::Feature. Currently always 2D (z = None).
- Add `convert::geom::to_wkt` to normalize any geo-traits geometry into
  `wkt::Wkt`, preserving coordinate dimensionality; use
  `geo_traits::to_geo::ToGeoGeometry` for the reverse at the 2D MVT/WKB
  boundary.
- Migrate all decoder-output consumers (geojson serde, MVT, mlt-py WKB,
  mlt TUI rendering) to read via wkt / geo-traits accessors.

Depends on `wkt` 0.14 (whose types implement geo-traits 0.3), keeping
the workspace `unsafe_code = "forbid"` intact (the trait impls' unsafe
lives inside the wkt crate; mlt-core writes none).

No wire-format change; encoding is byte-identical and the full test
suite passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Encode and decode 3D geometries end-to-end: an interleaved `x, y, z`
vertex buffer marked by a new `GeometryZ` column type.

- Add `ColumnType::GeometryZ` (the in-tile analog of the spec's
  `ComplexType::GEOMETRY_Z`) and a `CoordDim { Xy, Xyz }` enum carried by
  `RawGeometry` and `GeometryValues`. The column type, not the stream data,
  is the single source of truth for dimensionality; it is threaded
  ColumnType -> RawGeometry -> GeometryValues -> vertex stride.
- Generalize the componentwise-delta vertex codec to an arbitrary stride
  (`encode/decode_componentwise_delta(.., n_dims, ..)`); the plain Vec3
  layout is used for 3D. Morton/Hilbert dictionaries remain 2D-only and
  are skipped for 3D columns.
- Decoder builds `wkt::Wkt` with `Dimension::XYZ` and a populated Z;
  encoder reads Z from the input geometry's coordinates (column
  dimensionality is taken from the first geometry pushed).
- GeoJSON serde emits/parses `[x, y, z]` when present; MVT stays 2D.

The `CoordDim` enum keeps dimensionality type-safe at the API boundaries
while the low-level codec/vertex helpers take a plain `usize` stride.

2D tiles are unaffected and byte-identical; added 3D round-trip tests
(point/linestring/multipoint/polygon through the full `GeometryZ` wire
path, plus geojson `[x,y,z]` and a stride-3 codec test). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mlt-py's WKB encoder previously routed decoded geometry through geo_types
(2D), silently dropping Z. Rewrite `geom32_to_wkb` to read the
dimension-aware `wkt::Wkt` directly and emit ISO WKB with Z preserved
(type code `base + 1000`, an extra f64 per coordinate) for 3D geometries;
2D output is unchanged. The tile transform applies to x/y only — z
(elevation) is written as-is. MVT stays 2D (it has no Z).

Adds a 3D WKB round-trip test.

Note: the shared tileset-metadata schema (spec/schema/*.proto) and the
written spec are intentionally left untouched — 3D remains a Rust-only
experiment for now, so the Java metadata path is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 04:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a Rust proof-of-concept implementation of 3D geometry support by introducing a GeometryZ column type and switching decoded geometry output to a dimension-aware representation (wkt::Wkt<i32>), enabling optional Z coordinates to survive encode/decode.

Changes:

  • Adds ColumnType::GeometryZ plus dimensionality tracking (CoordDim) and updates geometry encode/decode paths to support interleaved (x, y, z) vertices.
  • Migrates decoded geometry representation across the Rust UI, core, and Python bindings from geo_types::Geometry<i32> to wkt::Wkt<i32>, leveraging geo-traits.
  • Updates conversions (GeoJSON/MVT) and tests to cover 3D roundtrips and Z-preserving outputs.

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
rust/mlt/src/ui/state.rs UI selection/vertex extraction updated to wkt::Wkt APIs.
rust/mlt/src/ui/rendering/map.rs Renderer updated to draw wkt::Wkt geometries.
rust/mlt/src/ui/rendering/layers.rs Geometry stats panel updated for wkt::Wkt types/iterators.
rust/mlt/src/ui/mod.rs Geometry helper utilities updated for wkt polygon ring APIs.
rust/mlt/src/ui/mbt.rs World-vertex collection updated for wkt::Wkt.
rust/mlt-py/src/lib.rs Python WKB output updated to emit Z-aware WKB for 3D.
rust/mlt-py/src/encode/geojson.rs GeoJSON encoder updated for wkt::Wkt and builder API changes.
rust/mlt-core/src/lib.rs Re-exports geo_traits/wkt and exports CoordDim.
rust/mlt-core/src/encoder/sort.rs Sorting generalized to geo_traits::GeometryTrait traversal.
rust/mlt-core/src/encoder/property/tests.rs Tests updated to feed wkt geometry into tile features.
rust/mlt-core/src/encoder/geometry/tests.rs Adds end-to-end 3D encode→decode tests and updates parsing calls.
rust/mlt-core/src/encoder/geometry/geotype.rs Geometry ingestion generalized to geo_traits and extended to 3D vertices.
rust/mlt-core/src/encoder/geometry/encode.rs Vertex stream encoding generalized to N-dim stride; writes GeometryZ column type.
rust/mlt-core/src/decoder/stream/logical.rs Adds stride-aware decode helper for componentwise-delta vertex streams.
rust/mlt-core/src/decoder/stream/decode.rs Adds stride-aware stream decode wrapper for vertex buffers.
rust/mlt-core/src/decoder/root.rs Parses GeometryZ and carries dimension into raw geometry.
rust/mlt-core/src/decoder/model.rs Adds GeometryZ column type; switches tile geometry storage to wkt::Wkt.
rust/mlt-core/src/decoder/mod.rs Re-exports CoordDim and updates internal exports.
rust/mlt-core/src/decoder/iterators.rs Feature iterator now yields wkt::Wkt geometry.
rust/mlt-core/src/decoder/geometry/model.rs Introduces CoordDim; stores dim on raw/decoded geometry values.
rust/mlt-core/src/decoder/geometry/geotype.rs Decodes vertex buffers into dimensioned wkt::Wkt output.
rust/mlt-core/src/decoder/geometry/decode.rs Uses CoordDim to decode vertex buffer with correct stride.
rust/mlt-core/src/decoder/column.rs Updates column-name rules to include GeometryZ.
rust/mlt-core/src/convert/mvt/encode.rs Converts wkt::Wkt to 2D geo_types geometry for MVT boundary.
rust/mlt-core/src/convert/mvt/decode.rs Normalizes MVT decoded geometries into wkt::Wkt.
rust/mlt-core/src/convert/mod.rs Adds internal convert::geom module.
rust/mlt-core/src/convert/geom.rs New normalization utility from geo_traits geometries to wkt::Wkt.
rust/mlt-core/src/convert/geojson.rs GeoJSON serde updated to support emitting/reading [x,y,z] coords.
rust/mlt-core/src/codecs/zigzag.rs Generalizes componentwise-delta codec from vec2 to N-dim stride.
rust/mlt-core/Cargo.toml Adds geo-traits and wkt dependencies.
rust/Cargo.toml Adds workspace dependencies for geo-traits and wkt.
rust/Cargo.lock Locks new geo-traits/wkt dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 54 to 64
target.clear();
target.reserve(data.len());
let mut prev_x = T::zero();
let mut prev_y = T::zero();
for chunk in data.chunks_exact(2) {
let (x, y) = (chunk[0], chunk[1]);
target.push(T::encode(x.wrapping_sub(&prev_x)));
target.push(T::encode(y.wrapping_sub(&prev_y)));
(prev_x, prev_y) = (x, y);
debug_assert!(n_dims <= MAX_COMPONENTS);
let mut prev = [T::zero(); MAX_COMPONENTS];
for chunk in data.chunks_exact(n_dims) {
for (j, &v) in chunk.iter().enumerate() {
target.push(T::encode(v.wrapping_sub(&prev[j])));
prev[j] = v;
}
}
target
Comment on lines +310 to +316
// Strip the closing vertex if the ring repeats its first coordinate at the end.
let closed = n > 1
&& ring
.coord(0)
.zip(ring.coord(n - 1))
.is_some_and(|(f, l)| f.x() == l.x() && f.y() == l.y());
let len = if closed { n - 1 } else { n };
Comment on lines +124 to +130
if self.vector_types.is_empty() {
self.dim = if matches!(geom.dim(), Dimensions::Xyz) {
CoordDim::Xyz
} else {
CoordDim::Xy
};
}
Comment on lines +171 to +174
Wkt::Point(p) => (
"Point",
serde_json::to_value(point_arr(p).unwrap_or_default()).unwrap(),
),
Comment on lines +142 to +146
fn arr_coord(a: &Arr) -> Coord<i32> {
Coord {
x: a[0],
y: a[1],
z: a.get(2).copied(),
Comment on lines 223 to 226
pub(crate) id: Option<u64>,
/// Geometry as a [`geo_types`] form
pub(crate) geometry: geo_types::Geometry<i32>,
/// Geometry as a dimension-aware [`wkt::Wkt`] (currently always 2D).
pub(crate) geometry: wkt::Wkt<i32>,
/// One value per property column, in the same order as
Comment on lines +231 to +233
/// Decoded geometry as a dimension-aware [`wkt::Wkt`] (owned, decoded on demand by the
/// iterator). Currently always 2D.
geometry: wkt::Wkt<i32>,
Comment thread rust/mlt-py/src/lib.rs
Comment on lines +104 to 107
fn geom32_to_wkb(geom: &Wkt<i32>, xf: Option<TileTransform>) -> MltResult<Vec<u8>> {
let has_z = matches!(geom.dimension(), Dimension::XYZ);
let mut buf = Vec::with_capacity(128);
match geom {
Comment on lines +3 to +9
//! MLT accepts any geometry that implements [`geo_traits::GeometryTrait`] as encoder input
//! (so both [`geo_types`] and [`wkt`] values work), and produces [`wkt::Wkt`] as decoder output
//! (the dimension-aware type that can carry an optional Z coordinate).
//!
//! [`to_wkt`] normalizes any geo-traits geometry into a [`wkt::Wkt`], preserving coordinate
//! dimensionality. [`geo_traits::to_geo::ToGeoGeometry::to_geometry`] handles the reverse
//! (`wkt::Wkt` -> `geo_types::Geometry`, dropping any Z) for the 2D MVT boundary.
@github-actions

Copy link
Copy Markdown
Contributor
Performance Comparison

Performance Comparison mainfeat/rust-3d

Total Elapsed Time: 2.22s → 2.32s (+4.3%)
CPU Baseline: 80.84µs → 80.99µs (+0.2%)
Benchmark ID: mlt-convert

timing - Function execution time metrics.

+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| Function                             | Calls                        | Avg                             | P95                             | Total                            | % Total                      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| optimizer::encode                    | 14096 → 14096 (+0.0%)        | 526.20µs → 538.69µs (+2.4%)     | 2.29ms → 2.33ms (+1.7%)         | 7.42s → 7.59s (+2.3%)            | 333.66% → 327.53% (-1.8%)    |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| optimizer::encode_into               | 28192 → 28192 (+0.0%)        | 168.06µs → 169.44µs (+0.8%)     | 718.85µs → 730.11µs (+1.6%)     | 4.74s → 4.78s (+0.8%)            | 213.14% → 206.05% (-3.3%)    |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| encode::write_to                     | 28192 → 28192 (+0.0%)        | 133.55µs → 135.32µs (+1.3%)     | 638.46µs → 648.70µs (+1.6%)     | 3.77s → 3.82s (+1.3%)            | 169.36% → 164.56% (-2.8%)    |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| writer::with                         | 1573034 → 1573034 (+0.0%)    | 1.63µs → 1.66µs (+1.8%)         | 2.15µs → 2.15µs (+0.0%)         | 2.56s → 2.61s (+2.0%)            | 115.07% → 112.54% (-2.2%)    |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| mlt::main                            | 1 → 1 (+0.0%)                | 2.22s → 2.32s (+4.5%)           | 2.23s → 2.32s (+4.0%)           | 2.22s → 2.32s (+4.5%)            | 100.00% → 100.00% (+0.0%)    |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| optimizer::analyze                   | 14096 → 14096 (+0.0%)        | 120.78µs → 126.38µs (+4.6%)     | 628.22µs → 643.58µs (+2.4%)     | 1.70s → 1.78s (+4.7%)            | 76.59% → 76.84% (+0.3%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| shared_dict::group_string_properties | 14096 → 14096 (+0.0%)        | 116.24µs → 121.93µs (+4.9%)     | 600.58µs → 619.52µs (+3.2%)     | 1.64s → 1.72s (+4.9%)            | 73.71% → 74.14% (+0.6%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| encode::dict_may_be_beneficial       | 28192 → 28192 (+0.0%)        | 47.52µs → 48.15µs (+1.3%)       | 217.85µs → 216.19µs (-0.8%)     | 1.34s → 1.36s (+1.5%)            | 60.27% → 58.56% (-2.8%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| encode::write_properties             | 28192 → 28192 (+0.0%)        | 32.16µs → 31.69µs (-1.5%)       | 130.56µs → 128.77µs (-1.4%)     | 906.52ms → 893.41ms (-1.4%)      | 40.78% → 38.54% (-5.5%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| encode::write_prop                   | 116572 → 116572 (+0.0%)      | 7.70µs → 7.58µs (-1.6%)         | 16.03µs → 15.94µs (-0.6%)       | 897.16ms → 883.92ms (-1.5%)      | 40.36% → 38.13% (-5.5%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| tile::from_tile                      | 28192 → 28192 (+0.0%)        | 19.73µs → 24.94µs (+26.4%) ⚠️   | 81.98µs → 106.56µs (+30.0%) ⚠️  | 556.25ms → 703.05ms (+26.4%) ⚠️  | 25.02% → 30.33% (+21.2%) ⚠️  |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| shared_dict::write_shared_dict       | 24002 → 24002 (+0.0%)        | 23.61µs → 22.71µs (-3.8%)       | 49.98µs → 50.30µs (+0.6%)       | 566.62ms → 545.01ms (-3.8%)      | 25.49% → 23.51% (-7.8%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| strings::write_str_col               | 57070 → 57070 (+0.0%)        | 4.64µs → 4.78µs (+3.0%)         | 11.25µs → 11.13µs (-1.1%)       | 265.08ms → 273.07ms (+3.0%)      | 11.92% → 11.78% (-1.2%)      |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| strings::fsst_try_train              | 40536 → 40536 (+0.0%)        | 5.16µs → 4.64µs (-10.1%)        | 71.00ns → 70.00ns (-1.4%)       | 209.09ms → 188.30ms (-9.9%)      | 9.41% → 8.12% (-13.7%)       |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| 🆕 encode::build_hilbert_dict        | 0 → 362 (+100.0%) ⚠️         | 0.00ns → 230.88µs (+100.0%) ⚠️  | 0.00ns → 408.57µs (+100.0%) ⚠️  | 0.00ns → 83.58ms (+100.0%) ⚠️    | 0.00% → 3.61% (+100.0%) ⚠️   |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+
| 🗑️ sort::curve_params                | 14096 → 0 (-100.0%) 🚀       | 17.93µs → 0.00ns (-100.0%) 🚀   | 80.19µs → 0.00ns (-100.0%) 🚀   | 252.79ms → 0.00ns (-100.0%) 🚀   | 11.37% → 0.00% (-100.0%) 🚀  |
+--------------------------------------+------------------------------+---------------------------------+---------------------------------+----------------------------------+------------------------------+

alloc-bytes - Exclusive allocation bytes by each function (excluding nested calls).

+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| Function                             | Calls                        | Avg                             | P95                              | Total                          | % Total                      |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| optimizer::encode                    | 14096 → 14096 (+0.0%)        | 86.2 KB → 113.0 KB (+31.1%) ⚠️  | 159.2 KB → 281.5 KB (+76.8%) ⚠️  | 1.2 GB → 1.5 GB (+25.0%) ⚠️    | 23.85% → 29.09% (+22.0%) ⚠️  |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| tile::from_tile                      | 28192 → 28192 (+0.0%)        | 44.1 KB → 44.1 KB (+0.0%)       | 238.5 KB → 238.5 KB (+0.0%)      | 1.2 GB → 1.2 GB (+0.0%)        | 24.41% → 22.73% (-6.9%)      |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| writer::with                         | 1573034 → 1573034 (+0.0%)    | 723 B → 723 B (+0.0%)           | 25.0 KB → 25.0 KB (+0.0%)        | 1.1 GB → 1.1 GB (+0.0%)        | 21.79% → 20.29% (-6.9%)      |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| strings::fsst_try_train              | 40536 → 40536 (+0.0%)        | 16.8 KB → 16.8 KB (+0.0%)       | 3.6 MB → 3.6 MB (+0.0%)          | 664.5 MB → 664.5 MB (+0.0%)    | 13.35% → 12.43% (-6.9%)      |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| shared_dict::group_string_properties | 14096 → 14096 (+0.0%)        | 30.3 KB → 30.3 KB (+0.0%)       | 171.6 KB → 171.6 KB (+0.0%)      | 416.9 MB → 416.9 MB (+0.0%)    | 8.37% → 7.80% (-6.8%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| encode::write_to                     | 28192 → 28192 (+0.0%)        | 7.5 KB → 7.5 KB (+0.0%)         | 35.7 KB → 35.7 KB (+0.0%)        | 206.0 MB → 206.0 MB (+0.0%)    | 4.14% → 3.85% (-7.0%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| shared_dict::write_shared_dict       | 24002 → 24002 (+0.0%)        | 3.4 KB → 3.4 KB (+0.0%)         | 11.6 KB → 11.6 KB (+0.0%)        | 80.9 MB → 80.9 MB (+0.0%)      | 1.62% → 1.51% (-6.8%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| strings::write_str_col               | 57070 → 57070 (+0.0%)        | 893 B → 893 B (+0.0%)           | 3.5 KB → 3.5 KB (+0.0%)          | 48.6 MB → 48.6 MB (+0.0%)      | 0.98% → 0.91% (-7.1%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| strings::write_raw_str_data          | 137744 → 137744 (+0.0%)      | 122 B → 122 B (+0.0%)           | 9.0 KB → 9.0 KB (+0.0%)          | 16.1 MB → 16.1 MB (+0.0%)      | 0.32% → 0.30% (-6.2%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| encode::build_morton_dict            | 362 → 362 (+0.0%)            | 44.6 KB → 44.6 KB (+0.0%)       | 89.9 KB → 89.9 KB (+0.0%)        | 15.8 MB → 15.8 MB (+0.0%)      | 0.32% → 0.29% (-9.4%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| optimizer::analyze                   | 14096 → 14096 (+0.0%)        | 1.0 KB → 1.0 KB (+0.0%)         | 6.5 KB → 6.5 KB (+0.0%)          | 14.0 MB → 14.0 MB (+0.0%)      | 0.28% → 0.26% (-7.1%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| encode::build_hilbert_dict           | 362 → 362 (+0.0%)            | 25.9 KB → 25.9 KB (+0.0%)       | 90.7 KB → 90.7 KB (+0.0%)        | 9.2 MB → 9.2 MB (+0.0%)        | 0.18% → 0.17% (-5.6%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| encode::dict_may_be_beneficial       | 28192 → 28192 (+0.0%)        | 256 B → 256 B (+0.0%)           | 256 B → 256 B (+0.0%)            | 6.9 MB → 6.9 MB (+0.0%)        | 0.14% → 0.13% (-7.1%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| strings::write_str_plain             | 57070 → 57070 (+0.0%)        | 69 B → 69 B (+0.0%)             | 240 B → 240 B (+0.0%)            | 3.8 MB → 3.8 MB (+0.0%)        | 0.08% → 0.07% (-12.5%)       |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+
| encode::write_prop                   | 116572 → 116572 (+0.0%)      | 27 B → 27 B (+0.0%)             | 256 B → 256 B (+0.0%)            | 3.1 MB → 3.1 MB (+0.0%)        | 0.06% → 0.06% (+0.0%)        |
+--------------------------------------+------------------------------+---------------------------------+----------------------------------+--------------------------------+------------------------------+

Threads

No threads to compare


Generated with hotpath-rs

Per-tile encoded size diff
zoom tiles prev_total curr_total delta_total avg_delta pct best worst
0 1 71471 71471 0 0.0 0.0
1 4 220986 220986 0 0.0 0.0
2 16 615003 615003 0 0.0 0.0
3 64 1653851 1653851 0 0.0 0.0
4 256 3828904 3828904 0 0.0 0.0
5 1024 9716821 9716821 0 0.0 0.0
6 4096 32401914 32401914 0 0.0 0.0
ALL 5461 48508950 48508950 0 0.0 0.0

Top 3 improvements

Top 3 degradations

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.83249% with 285 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.01%. Comparing base (3139b05) to head (a89a0a9).
⚠️ Report is 34 commits behind head on main.

Files with missing lines Patch % Lines
rust/mlt/src/ui/rendering/map.rs 0.00% 47 Missing ⚠️
rust/mlt/src/ui/rendering/layers.rs 0.00% 40 Missing ⚠️
rust/mlt-core/src/convert/geom.rs 60.22% 35 Missing ⚠️
rust/mlt/src/ui/mod.rs 0.00% 31 Missing ⚠️
rust/mlt/src/ui/mbt.rs 0.00% 28 Missing ⚠️
rust/mlt/src/ui/state.rs 0.00% 26 Missing ⚠️
rust/mlt-core/src/encoder/sort.rs 74.11% 22 Missing ⚠️
rust/mlt-py/src/lib.rs 72.85% 19 Missing ⚠️
rust/mlt-py/src/encode/geojson.rs 0.00% 17 Missing ⚠️
rust/mlt-core/src/encoder/geometry/geotype.rs 87.71% 14 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1507      +/-   ##
==========================================
+ Coverage   62.82%   65.01%   +2.18%     
==========================================
  Files          94       96       +2     
  Lines       15317    16015     +698     
==========================================
+ Hits         9623    10412     +789     
+ Misses       5694     5603      -91     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CommanderStorm

Copy link
Copy Markdown
Member

and optional z

Since adding the new type would likely be a bit breaking for existing consumers of mlt@v1, we need to remove this part and re-add it in v2 😉
Which means we can do it properly and get the 3D data support right imo.

Would this be a good alternative for you?

@CommanderStorm
CommanderStorm marked this pull request as draft July 15, 2026 10:41
@yutannihilation

Copy link
Copy Markdown
Contributor Author

Thanks. I don't have strong opinion here, but let me clarify a bit for discussion.

Since adding the new type would likely be a bit breaking for existing consumers of mlt@v1

In my understanding, adding the ability to decode/encode GeometryZ shouldn't hurt anything; it's not like adding a new field. As the X and Y coordinates are already stored interleaved in a VertexBuffer, adding Z doesn't need additional field.

If you mean that the TypeScript decoder fails when the data contains GeometryZ, I don't think this is a problem. Not all decoders keep up with all encoders. For example, not all decoders implement FastPFOR, but we find FastPFOR is useful.

That said,

we need to remove this part and re-add it in v2

this sounds good to me. One of the things I feel uneasy with is the gap between the specification and the implementation, so it sounds a reasonable idea to tweak the specification to match with the implementation. If v2 will come in the foreseeable future, I'm happy with your idea!

@CommanderStorm

Copy link
Copy Markdown
Member

I have a hunch that I might get the time to work towards V2 after my exams this way or that. ^^

@yutannihilation

Copy link
Copy Markdown
Contributor Author

Good luck on your exam!

@nyurik

nyurik commented Jul 15, 2026

Copy link
Copy Markdown
Member

@yutannihilation I didn't go too far into the implementation yet, but I do want to mention a few direction thougths.

  • we should not make any "logical" changes to MLTv1 -- v1 should have a feature parity with MVT - i.e. it should be possible to roundtrip MVT->MLTv1->MVT or to convert any MLTv1 to MVT without data loss. There are a few minor exceptions to this due to columnar nature of the storage, but it should work for pretty much every regular tile. Moreover, the set of v1 compressions and specifications should never change - so if a lib declares MLTv1 support, every single v1 will be supported going further.
  • MLT has a framing concept - so it is possible to mix in multiple different frames one after another - so if v2 supports 3D, all parsers will know what to do -- either parse it (if they support v2), or skip it (because they know the size)
  • we are currently trying to figure out the v2 support in v2 optimization and feature ideas #1201 - it could include the Z coordinates as either additional geometry sub-stream or as M-value (or both). I'm all for figuring it out. We will just need to make sure our renderers would be able to handle it properly. And of course, as you mentioned, there is a long desired but highly uncertain MLT 3D data support #1182 - we do want the proper 3D, we just don't have a finalized implementation. It will most likely have to go into a separate frame number, e.g. v3

@yutannihilation

yutannihilation commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for sharing the directions! But, let me make it clear about this point.

we should not make any "logical" changes to MLTv1

I don't change anything. The current MLT specification supports 3D coordinates, and I just naively implemented it. I understand "a feature parity with MVT" is your current consensus on v1, but it's not what the specification says.

@nyurik

nyurik commented Jul 15, 2026

Copy link
Copy Markdown
Member

@yutannihilation I am not saying you did anything wrong :) The specification was written long before we started v1 implementation, and it was initially trying many things at once. Later, we tried to clean it up to limit just the MVT-logical-compat for v1 - but we might have missed a thing or two - that should be clearly marked as the future development rather than part of the v1 specification. I will update it to clearly state that the current v1 implementation is only 2D, whereas we can easily add the 3D support later, using either the method mentioned in the spec, or we could alter it significantly - none of the z support is set in stone yet.

@nyurik

nyurik commented Jul 15, 2026

Copy link
Copy Markdown
Member

See #1509

@yutannihilation

Copy link
Copy Markdown
Contributor Author

Ah, makes sense. Thanks for the context!

nyurik added a commit to nyurik/maplibre-tile-spec that referenced this pull request Jul 16, 2026
@yutannihilation

Copy link
Copy Markdown
Contributor Author

I forgot to explain one caveat in this implementation; in order to use only what the specification document says, this implementation stores Z coordinate as-is. But, it's not obvious what this value should be translated.

X and Y are in-tile coordinates. This means these have implicit scale and offset, which are uniquely determined by the combination of X, Y, and zoom level. But, it's not the case with Z coordinate. So, I wish v2 define some metadata like z_scale and z_offset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants