Use from Rust
[dependencies]ezu = { version = "0.6", features = ["parallel"] }The umbrella crate re-exports the workspace: ezu::style, ezu::graph,
ezu::paint, ezu::features, ezu::core. API detail lives on
docs.rs; this page is the shape of a host.
The lifecycle
Section titled “The lifecycle”Parse once, build once, render per tile:
let doc = ezu::style::Document::from_json(&json)?;let registry = ezu::paint::nodes::default_registry();let graph = ezu::graph::build_graph(&doc, ®istry)?; // once per stylelet cache = ezu::graph::Cache::new(); // shared across tileslet base = ezu::paint::host::BrushBankLoader::default();
let tile_id = ezu::graph::TileId { z: 13, x: 7276, y: 3225 };
// Per-tile bindings overlay the document-scoped bank.let mut tile_loader = ezu::paint::host::TileLoader::new(&base, tile_id);tile_loader.bind_mvt(ezu::features::mvt::decode(&bytes)?);
// The style's `pad` is a floor; the graph knows how far its filters// reach. Sizing the canvas from both is what the CLI and the wasm host do.let canvas = ezu::graph::CanvasInfo { tile_size: doc.tile_size, pad: doc.pad.max(graph.required_pad()?),};
let ev = ezu::graph::Evaluator::new(&graph, &cache, &tile_loader);let out = ev.render_parallel( tile_id, canvas, &ezu::graph::ParamValues::new(), /* rng_seed */ 0,)?;build_graph is where everything static is checked: every @ref resolves, ports
type-check, there are no cycles, and every field that decides padding carries a
static bound. Failures come
back as BuildGraphError with the offending node id attached — surface that
string, it is the useful part.
You own the I/O
Section titled “You own the I/O”ezu never fetches anything itself. External data enters through one trait:
pub trait AssetLoader: Send + Sync { fn load(&self, name: &str) -> Result<Asset, AssetError>; fn hash(&self, _name: &str) -> u128 { 0 } // for cache invalidation}Think of it as shader uniforms: the document declares which bindings its source
nodes sample, your host fills them, the evaluator stitches it together. Names
beginning with tile. are tile-scoped by convention and rebound per render;
bare names are document-scoped (brush and image banks).
Return a real hash() for anything that can change. The evaluator folds each
binding’s hash into the consuming node’s cache key, so a changed binding
invalidates every dependent entry without the node having to think about it.
For the common cases you do not need to implement the trait yourself:
ezu::paint::host ships BrushBankLoader, TileLoader,
prefetch_doc_assets, DEM/raster source registries, and PNG/WebP encoders.
Bind and render, as the JS and Go packages do
Section titled “Bind and render, as the JS and Go packages do”The lifecycle above is the whole machine. When your host already does its own
fetching and only wants to bind bytes and get pixels back, ezu::renderer is
the same embedding API the JS and
Go packages wrap, with the same calls:
use ezu::renderer::{BindOptions, RenderOptions, Renderer};
let mut r = Renderer::new(&style_json)?;r.bind_source("basemap", mvt_bytes, &BindOptions::default())?;let png = r.render_tile(13, 7276, 3225, RenderOptions::default())?;r.clear_sources();A page at a print scale is compute_frame and render_frame, which render the
tiles the page covers at the tile size that meets the scale and crop them into
one RGBA page, the same as ezu frame:
use ezu::renderer::FrameRequest;
let f = r.compute_frame(&FrameRequest { center_lon: 139.70, center_lat: 35.68, scale: 1000.0, dpi: 200.0, width: 1929, height: 1370, // 245 × 174 mm at 200 dpi zoom: None, style_tile_size: r.tile_size(),})?;let page = r.render_frame(&f, RenderOptions::default(), |b, tile| { let mvt = fetch_mvt(tile.z, tile.x, tile.y)?; // your fetch, your error type b.bind_source("basemap", mvt, &BindOptions::default())?; Ok::<_, MyError>(())})?;The bind gets a FrameBinder for the tile it is binding. render_frame holds
the renderer by &mut for the whole frame, so that binder is the only way to
bind while it runs — the borrow checker does what the JS and Go packages check at
run time.
Serial or parallel
Section titled “Serial or parallel”Evaluator::render walks the topological order sequentially.
render_parallel (behind the parallel feature) buckets nodes by longest-path
depth — no two nodes in a bucket share an edge — and fans each bucket across
Rayon’s pool. On a wide watercolour graph that is the difference between 6.3 s and
1.3 s of wall clock for four tiles.
Leave parallel off for wasm; turn it on for servers and CLIs.
Params at render time
Section titled “Params at render time”One built graph serves every parameter combination:
let mut params = ezu::graph::ParamValues::new();params.set("softness".into(), ezu::graph::parse_param_value(&doc.params, "softness", "2")?);let out = ev.render_parallel(tile_id, canvas, ¶ms, 0)?;The cache keys on the values each node actually reads, so flipping one param re-evaluates only the nodes downstream of it.
Cache sizing
Section titled “Cache sizing”let cache = ezu::graph::Cache::with_capacity(4096); // entries, not bytesA Mutex-protected LRU keyed by a Merkle-style content hash over
(canvas, tile, node param hash + asset hashes, input hashes). Hits are cheap to
clone because heavy payloads sit behind Arc. Keep one cache per style for the
life of that style; throw it away when the style changes.
Your own ops
Section titled “Your own ops”NodeFactory is public, so a downstream crate can register ops on top of the
built-in registry and inherit the JSON Schema (and therefore editor completion)
automatically. See custom ops.
Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.