use std::{
future::Future,
ops::Deref,
sync::{Arc, atomic::AtomicBool},
};
use anyhow::anyhow;
use ccutils::{
cache::Cache,
log::{log_error, log_error_and_continue},
};
use futures::{
SinkExt, StreamExt,
channel::{mpsc, oneshot},
executor::block_on,
};
use geo::{Rect, coord};
use parking_lot::RwLock;
use parley::{FontContext, Layout, LayoutContext, PositionedLayoutItem, StyleProperty};
use vello::{
kurbo::{self, Affine},
peniko::{self, Brush, ImageBrush, ImageData, ImageFormat},
util::{RenderContext, block_on_wgpu},
wgpu::{self},
};
use crate::{
BoxedImageDataRef, ImageFeature, Result,
renderer::{self, MapRenderer, ViewController},
styling,
};
pub struct Texture
{
device: wgpu::Device,
texture: wgpu::Texture,
}
impl Texture
{
fn new(device: wgpu::Device, width: u32, height: u32) -> Self
{
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&vello::wgpu::TextureDescriptor {
label: Some("vello/texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
});
Self { device, texture }
}
fn is_size(&self, width: u32, height: u32) -> bool
{
width == self.texture.width() && height == self.texture.height()
}
pub fn create_texture_view(&self, desc: &wgpu::TextureViewDescriptor) -> wgpu::TextureView
{
self.texture.create_view(desc)
}
fn create_vello_texture_view(&self) -> wgpu::TextureView
{
self.create_texture_view(&wgpu::TextureViewDescriptor::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct CpuImage
{
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
type LayerDrawer =
Box<dyn for<'a> Fn(&mut SceneBuilder<'a>, &renderer::ViewController, bool, bool) + Send + Sync>;
struct LayerEntry
{
z_order: i32,
insertion_order: u64,
is_primary: bool,
draw: LayerDrawer,
}
impl LayerEntry
{
fn sort_key(&self) -> (i32, u64)
{
(self.z_order, self.insertion_order)
}
}
fn make_layer_entry<TFeature: crate::Feature + 'static>(
map: Arc<crate::Map<TFeature>>,
style: styling::Style<TFeature>,
z_order: i32,
insertion_order: u64,
is_primary: bool,
) -> LayerEntry
{
LayerEntry {
z_order,
insertion_order,
is_primary,
draw: Box::new(
move |scene_builder, view_controller, reset_scene, draw_background| {
scene_builder.set_layer_mode(reset_scene, draw_background);
scene_builder.draw_map(map.as_ref(), view_controller, &style);
},
),
}
}
const VELLO_MAX_DRAWS: u64 = 1 << 21;
const VELLO_MAX_SEGMENTS: u64 = 1 << 23;
const VELLO_MAX_BIN_DATA: u64 = 1 << 18;
struct RendererCommon
{
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
renderer: vello::Renderer,
scene: vello::Scene,
texture: Texture,
vello_texture_view: wgpu::TextureView,
vello_image_cache: Cache<u64, ImageData>,
}
impl RendererCommon
{
fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, texture: Texture) -> Result<Self>
{
let renderer = vello::Renderer::new(
&device,
vello::RendererOptions {
use_cpu: cfg!(target_os = "windows"),
antialiasing_support: vello::AaSupport::all(),
num_init_threads: None,
pipeline_cache: None,
},
)?;
let scene = Default::default();
let vello_texture_view = texture.create_vello_texture_view();
Ok(Self {
device,
queue,
renderer,
scene,
texture,
vello_texture_view,
vello_image_cache: Default::default(),
})
}
fn update(&mut self, view_controller: &ViewController, layers: &mut [LayerEntry]) -> Result<()>
{
let view_rect = view_controller.view_rect();
let map_rect = view_controller.map_rect();
log::debug!(
"vello update start layers={} tex={}x{} view_rect=({:.2},{:.2})-({:.2},{:.2}) map_rect=({:.5},{:.5})-({:.5},{:.5})",
layers.len(),
self.texture.texture.width(),
self.texture.texture.height(),
view_rect.min().x,
view_rect.min().y,
view_rect.max().x,
view_rect.max().y,
map_rect.min().x,
map_rect.min().y,
map_rect.max().x,
map_rect.max().y,
);
self.vello_image_cache.garbage_collect_and_reset();
let mut scene_builder =
crate::vello::SceneBuilder::new(&mut self.scene, &mut self.vello_image_cache);
layers.sort_by_key(LayerEntry::sort_key);
for (index, layer) in layers.iter().enumerate()
{
log::debug!(
"vello layer build index={} z_order={} insertion_order={} is_primary={} reset_scene={} draw_background={}",
index,
layer.z_order,
layer.insertion_order,
layer.is_primary,
index == 0,
layer.is_primary
);
(layer.draw)(
&mut scene_builder,
view_controller,
index == 0,
layer.is_primary,
);
}
let stats = scene_builder.stats();
log::debug!("vello scene build stats: {:?}", stats);
if !stats.is_within_vello_limits()
{
let draws = stats.estimated_draws();
let segments = stats.estimated_segments();
log::error!(
"vello budget exceeded — skipping render: draws={} (limit {}) segments={} (limit {})",
draws,
VELLO_MAX_DRAWS,
segments,
VELLO_MAX_SEGMENTS
);
return Err(anyhow!("vello scene budget exceeded"));
}
log::debug!(
"vello render_to_texture start tex={}x{} aa={:?}",
self.texture.texture.width(),
self.texture.texture.height(),
vello::AaConfig::Msaa8
);
self.renderer.render_to_texture(
&self.device,
&self.queue,
&self.scene,
&self.vello_texture_view,
&vello::RenderParams {
base_color: vello::peniko::Color::TRANSPARENT,
width: self.texture.texture.width(),
height: self.texture.texture.height(),
antialiasing_method: vello::AaConfig::Msaa8,
},
)?;
log::debug!("vello render_to_texture done");
Ok(())
}
fn set_texture(&mut self, texture: Texture)
{
self.texture = texture;
self.vello_texture_view = self.texture.create_vello_texture_view();
}
fn to_cpu_image(&self) -> Result<CpuImage>
{
let width = self.texture.texture.width();
let height = self.texture.texture.height();
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let padded_byte_width = (width * 4).next_multiple_of(256);
let buffer_size = padded_byte_width as u64 * height as u64;
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Export Buffer"),
size: buffer_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Copy out buffer"),
});
encoder.copy_texture_to_buffer(
self.texture.texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_byte_width),
rows_per_image: None,
},
},
size,
);
self.queue.submit([encoder.finish()]);
let buf_slice = buffer.slice(..);
let (sender, receiver) = oneshot::channel();
buf_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
block_on_wgpu(self.device.as_ref(), receiver)??;
let data = buf_slice.get_mapped_range();
let mut result_unpadded = Vec::<u8>::with_capacity((width * height * 4).try_into()?);
for row in 0..height
{
let start = (row * padded_byte_width).try_into()?;
result_unpadded.extend(&data[start..start + (width * 4) as usize]);
}
Ok(CpuImage {
width,
height,
data: result_unpadded,
})
}
}
pub struct Renderer<TFeature: crate::Feature>
{
renderer_common: RendererCommon,
view_controller: renderer::ViewController,
layers: Vec<LayerEntry>,
_phantom: std::marker::PhantomData<TFeature>,
}
impl<TFeature: crate::Feature + 'static> Renderer<TFeature>
{
pub fn new_device() -> Result<(wgpu::Device, wgpu::Queue)>
{
ThreadedRenderer::new_device()
}
pub fn new(
device: wgpu::Device,
queue: wgpu::Queue,
map: Arc<crate::Map<TFeature>>,
style: styling::Style<TFeature>,
) -> Result<Self>
{
let width = 512;
let height = 512;
let texture = Texture::new(device.to_owned(), width, height);
let device = Arc::new(device);
let queue = Arc::new(queue);
let renderer_common = RendererCommon::new(device, queue, texture)?;
let view_controller = renderer::ViewController::show_extent(
Rect::new(coord! {x: -180.0, y: -80.0}, coord! { x: 180.0, y: 80.0 }),
Rect::new(coord! {x: 0.0, y: 0.0}, coord! { x: 512.0, y: 512.0 }),
);
let layers = vec![make_layer_entry(map, style, 0, 0, true)];
Ok(Self {
renderer_common,
view_controller,
layers,
_phantom: std::marker::PhantomData,
})
}
pub fn update(&mut self) -> Result<()>
{
self
.renderer_common
.update(&self.view_controller, &mut self.layers)?;
Ok(())
}
pub fn create_texture_view(&self, desc: &wgpu::TextureViewDescriptor) -> wgpu::TextureView
{
self.renderer_common.texture.create_texture_view(desc)
}
pub fn resize(&mut self, width: u32, height: u32) -> bool
{
if !self.renderer_common.texture.is_size(width, height)
{
let texture = Texture::new(self.renderer_common.texture.device.clone(), width, height);
self.renderer_common.set_texture(texture);
self.view_controller.set_view(Rect::new(
coord! {x: 0.0, y: 0.0},
coord! { x: width as f64, y: height as f64},
));
log_error!(
self
.renderer_common
.update(&self.view_controller, &mut self.layers),
"during update"
);
true
}
else
{
false
}
}
pub fn update_view_controller(&mut self, u: impl Fn(&mut renderer::ViewController))
{
u(&mut self.view_controller)
}
pub fn use_view_controller<T>(&self, u: impl Fn(&renderer::ViewController) -> T) -> T
{
u(&self.view_controller)
}
}
type TextureChanged = Box<dyn Fn(&Texture) + Sync + Send + 'static>;
enum TRMessages
{
Update,
AddOverlay
{
layer: LayerEntry,
},
UpdateOverlay
{
z_order: i32,
},
ResizeTexture
{
width: u32,
height: u32,
texture_changed: Option<TextureChanged>,
},
ImageSubscriber
{
sender: crossbeam::channel::Sender<CpuImage>,
},
}
pub struct ThreadedRenderer
{
view_controller: Arc<RwLock<renderer::ViewController>>,
sender: futures::channel::mpsc::Sender<TRMessages>,
width: u32,
height: u32,
is_rendering_image: Arc<AtomicBool>,
next_layer_order: u64,
}
impl ThreadedRenderer
{
pub fn new_device() -> Result<(wgpu::Device, wgpu::Queue)>
{
block_on(Self::new_device_async())
}
pub async fn new_device_async() -> Result<(wgpu::Device, wgpu::Queue)>
{
let mut context = RenderContext::new();
let device_id = context
.device(None)
.await
.ok_or_else(|| anyhow!("Error creating device."))?;
let device_handle = &mut context.devices[device_id];
let device = &device_handle.device;
let queue = &device_handle.queue;
Ok((device.clone(), queue.clone()))
}
pub fn new_thread<TFeature: crate::Feature + 'static>(
device: wgpu::Device,
queue: wgpu::Queue,
map: Arc<crate::Map<TFeature>>,
style: styling::Style<TFeature>,
) -> Result<Self>
{
let (s, f) = Self::new_async(device, queue, map, style)?;
std::thread::spawn(move || {
block_on(f);
});
Ok(s)
}
fn new_async<TFeature: crate::Feature + 'static>(
device: wgpu::Device,
queue: wgpu::Queue,
map: Arc<crate::Map<TFeature>>,
style: styling::Style<TFeature>,
) -> Result<(Self, impl Future<Output = ()> + 'static)>
{
let width = 512;
let height = 512;
let texture = Texture::new(device.clone(), width, height);
let is_rendering_image = Arc::<AtomicBool>::default();
let mut layers = vec![make_layer_entry(map, style, 0, 0, true)];
let view_controller = Arc::new(RwLock::new(renderer::ViewController::show_extent(
Rect::new(coord! { x: -180.0, y: -80.0}, coord! { x: 180.0, y: 80.0}),
Rect::new(coord! { x: 0.0, y: 0.0}, coord! { x: 512.0, y: 512.0}),
)));
let (sender, mut receiver) = mpsc::channel::<TRMessages>(100);
let device = Arc::new(device);
let queue = Arc::new(queue);
let fut = {
let view_controller = view_controller.clone();
let mut renderer_common = RendererCommon::new(device, queue, texture)?;
let is_rendering_image = is_rendering_image.clone();
async move {
let mut image_subscribers = Vec::default();
loop
{
let m = receiver.next().await;
match m
{
Some(m) => match m
{
TRMessages::ResizeTexture {
width,
height,
texture_changed,
} =>
{
if !renderer_common.texture.is_size(width, height)
{
is_rendering_image.store(true, std::sync::atomic::Ordering::Relaxed);
let texture = Texture::new(renderer_common.texture.device.clone(), width, height);
renderer_common.set_texture(texture);
log_error_and_continue!(
renderer_common.update(&view_controller.read(), &mut layers),
"During update"
);
is_rendering_image.store(false, std::sync::atomic::Ordering::Relaxed);
if let Some(texture_changed) = texture_changed
{
texture_changed(&renderer_common.texture);
}
log_error_and_continue!(
Self::send_image(&renderer_common, &mut image_subscribers),
"While sending images"
);
}
}
TRMessages::Update =>
{
is_rendering_image.store(true, std::sync::atomic::Ordering::Relaxed);
log_error_and_continue!(
renderer_common.update(&view_controller.read(), &mut layers),
"During updat"
);
is_rendering_image.store(false, std::sync::atomic::Ordering::Relaxed);
log_error_and_continue!(
Self::send_image(&renderer_common, &mut image_subscribers),
"While sending images"
);
}
TRMessages::AddOverlay { layer } =>
{
layers.push(layer);
is_rendering_image.store(true, std::sync::atomic::Ordering::Relaxed);
log_error_and_continue!(
renderer_common.update(&view_controller.read(), &mut layers),
"During update with overlay"
);
is_rendering_image.store(false, std::sync::atomic::Ordering::Relaxed);
log_error_and_continue!(
Self::send_image(&renderer_common, &mut image_subscribers),
"While sending images"
);
}
TRMessages::UpdateOverlay { z_order } =>
{
if !layers.iter().any(|layer| layer.z_order == z_order)
{
log::warn!("No overlay layer found for z_order={z_order}");
}
is_rendering_image.store(true, std::sync::atomic::Ordering::Relaxed);
log_error_and_continue!(
renderer_common.update(&view_controller.read(), &mut layers),
"During overlay update"
);
is_rendering_image.store(false, std::sync::atomic::Ordering::Relaxed);
log_error_and_continue!(
Self::send_image(&renderer_common, &mut image_subscribers),
"While sending images"
);
}
TRMessages::ImageSubscriber { sender } =>
{
image_subscribers.push(sender);
}
},
None => return,
}
}
}
};
let s = Self {
view_controller,
sender,
width,
height,
is_rendering_image,
next_layer_order: 1,
};
Ok((s, fut))
}
fn send_image(
render_common: &RendererCommon,
subscribers: &mut [crossbeam::channel::Sender<CpuImage>],
) -> Result<()>
{
match subscribers.len()
{
0 =>
{}
1 =>
{
let cpu_image = render_common.to_cpu_image()?;
subscribers[0].send(cpu_image)?;
}
_ =>
{
let cpu_image = render_common.to_cpu_image()?;
for s in subscribers.iter_mut()
{
s.send(cpu_image.clone())?;
}
}
}
Ok(())
}
pub fn is_rendering_map(&self) -> bool
{
self
.is_rendering_image
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn update(&mut self) -> Result<()>
{
block_on(self.update_async())?;
Ok(())
}
pub async fn update_async(&mut self) -> Result<()>
{
Ok(self.sender.send(TRMessages::Update).await?)
}
pub fn add_overlay<TFeature: crate::Feature + 'static>(
&mut self,
map: Arc<crate::Map<TFeature>>,
style: styling::Style<TFeature>,
z_order: i32,
) -> Result<()>
{
block_on(self.add_overlay_async(map, style, z_order))
}
pub async fn add_overlay_async<TFeature: crate::Feature + 'static>(
&mut self,
map: Arc<crate::Map<TFeature>>,
style: styling::Style<TFeature>,
z_order: i32,
) -> Result<()>
{
let layer = make_layer_entry(map, style, z_order, self.next_layer_order, false);
self.next_layer_order += 1;
Ok(self.sender.send(TRMessages::AddOverlay { layer }).await?)
}
pub fn update_overlay(&mut self, z_order: i32) -> Result<()>
{
block_on(self.update_overlay_async(z_order))
}
pub async fn update_overlay_async(&mut self, z_order: i32) -> Result<()>
{
Ok(
self
.sender
.send(TRMessages::UpdateOverlay { z_order })
.await?,
)
}
pub fn resize(
&mut self,
width: u32,
height: u32,
texture_changed: Option<impl Fn(&Texture) + Sync + Send + 'static>,
) -> Result<()>
{
block_on(self.resize_async(width, height, texture_changed))
}
pub async fn resize_async(
&mut self,
width: u32,
height: u32,
texture_changed: Option<impl Fn(&Texture) + Sync + Send + 'static>,
) -> Result<()>
{
if self.width != width || self.height != height
{
self.width = width;
self.height = height;
self
.sender
.send(TRMessages::ResizeTexture {
width,
height,
texture_changed: texture_changed
.map(|tc| -> Box<dyn Fn(&Texture) + Sync + Send + 'static> { Box::new(tc) }),
})
.await?;
self.view_controller.write().set_view(Rect::new(
coord! { x: 0.0, y: 0.0},
coord! { x: width as f64, y: height as f64},
));
}
Ok(())
}
pub fn subscribe_to_cpu_image(&mut self) -> Result<crossbeam::channel::Receiver<CpuImage>>
{
let (sender, receiver) = crossbeam::channel::bounded(1);
block_on(self.sender.send(TRMessages::ImageSubscriber { sender }))?;
Ok(receiver)
}
pub fn update_view_controller(&mut self, u: impl Fn(&mut renderer::ViewController))
{
u(&mut self.view_controller.write())
}
pub fn use_view_controller<T>(&self, u: impl FnOnce(&renderer::ViewController) -> T) -> T
{
u(&self.view_controller.read())
}
}
#[derive(Debug, Default, Clone, Copy)]
struct SceneBuildStats
{
background_fills: u64,
points: u64,
polygons: u64,
polygon_vertices: u64,
line_strings: u64,
line_vertices: u64,
images: u64,
labels_attempted: u64,
labels_drawn: u64,
}
impl SceneBuildStats
{
fn estimated_draws(&self) -> u64
{
self.background_fills
+ self.points
+ self.polygons
+ self.line_strings
+ self.images
+ self.labels_drawn
}
fn estimated_segments(&self) -> u64
{
self.polygon_vertices + self.line_vertices
}
fn is_within_vello_limits(&self) -> bool
{
let draws = self.estimated_draws();
let segments = self.estimated_segments();
draws <= VELLO_MAX_DRAWS
&& segments <= VELLO_MAX_SEGMENTS
&& draws <= VELLO_MAX_BIN_DATA
}
}
#[cfg(test)]
mod scene_build_stats_tests
{
use super::*;
#[test]
fn scene_build_stats_estimated_draws_and_segments()
{
let stats = SceneBuildStats {
background_fills: 1,
polygons: 549_473,
polygon_vertices: 5_339_239,
..Default::default()
};
assert_eq!(stats.estimated_draws(), 549_474);
assert_eq!(stats.estimated_segments(), 5_339_239);
}
#[test]
fn over_budget_scene_is_detected()
{
let stats = SceneBuildStats {
polygons: 2_100_000,
polygon_vertices: 1_000,
..Default::default()
};
assert!(!stats.is_within_vello_limits());
}
#[test]
fn normal_scene_is_within_budget()
{
let stats = SceneBuildStats {
background_fills: 1,
polygons: 5_000,
polygon_vertices: 50_000,
line_strings: 2_000,
line_vertices: 20_000,
..Default::default()
};
assert!(stats.is_within_vello_limits());
}
#[test]
fn albania_sized_scene_exceeds_bin_data_budget()
{
let stats = SceneBuildStats {
background_fills: 1,
polygons: 549_473,
polygon_vertices: 5_339_394,
..Default::default()
};
assert!(!stats.is_within_vello_limits());
}
}
pub struct SceneBuilder<'a>
{
scene: &'a mut vello::Scene,
vello_image_cache: &'a mut Cache<u64, ImageData>,
drawn_label_anchors: Vec<geo::Point>,
reset_scene_on_start: bool,
draw_background: bool,
stats: SceneBuildStats,
}
impl<'a> SceneBuilder<'a>
{
pub fn new(scene: &'a mut vello::Scene, vello_image_cache: &'a mut Cache<u64, ImageData>)
-> Self
{
Self {
scene,
vello_image_cache,
drawn_label_anchors: Vec::new(),
reset_scene_on_start: true,
draw_background: true,
stats: SceneBuildStats::default(),
}
}
fn stats(&self) -> SceneBuildStats
{
self.stats
}
fn set_layer_mode(&mut self, reset_scene_on_start: bool, draw_background: bool)
{
self.reset_scene_on_start = reset_scene_on_start;
self.draw_background = draw_background;
}
}
trait Brushify
{
fn brushify(&self) -> Brush;
}
impl Brushify for crate::styling::Rgba
{
fn brushify(&self) -> Brush
{
Brush::Solid(self.deref().to_owned())
}
}
fn fill_elts(elts: &mut Vec<kurbo::PathEl>, ls: &geo::LineString)
{
let mut it = ls.points();
let pt = it.next().unwrap();
elts.push(kurbo::PathEl::MoveTo(kurbo::Point {
x: pt.x(),
y: pt.y(),
}));
for pt in it
{
elts.push(kurbo::PathEl::LineTo(kurbo::Point {
x: pt.x(),
y: pt.y(),
}));
}
}
trait Shapify
{
type Shape: kurbo::Shape;
fn shapify(&self) -> Self::Shape;
}
impl Shapify for geo::Polygon
{
type Shape = kurbo::BezPath;
fn shapify(&self) -> kurbo::BezPath
{
let mut elts = Default::default();
fill_elts(&mut elts, self.exterior());
self
.interiors()
.iter()
.for_each(|ls| fill_elts(&mut elts, ls));
kurbo::BezPath::from_vec(elts)
}
}
impl Shapify for geo::LineString
{
type Shape = kurbo::BezPath;
fn shapify(&self) -> kurbo::BezPath
{
let mut elts = Default::default();
fill_elts(&mut elts, self);
kurbo::BezPath::from_vec(elts)
}
}
impl<'a> SceneBuilder<'a>
{
const LABEL_COLLISION_RADIUS_PX: f64 = 24.0;
const LABEL_HALO_RADIUS_PX: f32 = 1.0;
fn draw_shape<TFeature: crate::Feature>(
&mut self,
rendering_state: &styling::RenderingState,
feature: &TFeature,
shape: &impl kurbo::Shape,
symbol: &styling::Symbol<TFeature>,
)
{
let fill_color = (symbol.fill_color)(rendering_state, feature);
if fill_color.alpha() > 0.0
{
self.scene.fill(
peniko::Fill::NonZero,
vello::kurbo::Affine::IDENTITY,
&fill_color.brushify(),
None,
&shape,
);
}
if symbol.stroke_width > 0.0
{
self.scene.stroke(
&kurbo::Stroke::new(symbol.stroke_width),
vello::kurbo::Affine::IDENTITY,
&(symbol.stroke_color)(rendering_state, feature).brushify(),
None,
&shape,
);
}
}
fn can_draw_label_at_anchor(&self, anchor: geo::Point) -> bool
{
let min_distance_sq = Self::LABEL_COLLISION_RADIUS_PX * Self::LABEL_COLLISION_RADIUS_PX;
!self.drawn_label_anchors.iter().any(|drawn| {
let dx = drawn.x() - anchor.x();
let dy = drawn.y() - anchor.y();
(dx * dx + dy * dy) < min_distance_sq
})
}
fn draw_glyph_run_with_brush(
&mut self,
run: &parley::Run<'_, [u8; 4]>,
glyphs: &[vello::Glyph],
brush: &Brush,
)
{
if glyphs.is_empty()
{
return;
}
self
.scene
.draw_glyphs(run.font())
.font_size(run.font_size())
.brush(brush)
.draw(peniko::Fill::NonZero, glyphs.iter().copied());
}
fn draw_image_data(&mut self, image_id: u64, image_data: &dyn crate::ImageData, target_rect: Rect)
{
let (img_px_w, img_px_h) = image_data.pixel_size();
let vello_image = self.vello_image_cache.get_or_insert(image_id, || {
let img_data = image_data.rgba_data();
let (width, height) = image_data.pixel_size();
ImageData {
data: img_data.into(),
format: ImageFormat::Rgba8,
width,
height,
alpha_type: peniko::ImageAlphaType::Alpha,
}
});
let img_px_w = img_px_w as f64;
let img_px_h = img_px_h as f64;
let target_min = target_rect.min();
let target_w = target_rect.width();
let target_h = target_rect.height();
let scale_x = target_w / img_px_w;
let scale_y = target_h / img_px_h;
let translate_x = target_min.x;
let translate_y = target_min.y;
let transform = Affine::new([scale_x, 0.0, 0.0, scale_y, translate_x, translate_y]);
let projected_w = img_px_w * scale_x.abs();
let projected_h = img_px_h * scale_y.abs();
if projected_w < 1.0 || projected_h < 1.0
{
log::warn!(
"Skip rendering image with projection size less than 1 pixel: {}x{}",
projected_w,
projected_h
);
return;
}
self.scene.draw_image(
ImageBrush {
image: vello_image,
sampler: Default::default(),
},
transform,
);
}
}
impl<'a, TFeature: crate::Feature> renderer::MapRenderer<TFeature> for SceneBuilder<'a>
{
fn start_map(&mut self)
{
if self.reset_scene_on_start
{
self.scene.reset();
self.drawn_label_anchors.clear();
}
}
fn draw_background(&mut self, background_color: &styling::Rgba, size: (f64, f64))
{
if !self.draw_background
{
return;
}
self.stats.background_fills += 1;
self.scene.fill(
peniko::Fill::NonZero,
vello::kurbo::Affine::IDENTITY,
&(*background_color).brushify(),
None,
&kurbo::Rect {
x0: 0.0,
y0: 0.0,
x1: size.0,
y1: size.1,
},
);
}
fn draw_point(
&mut self,
rendering_state: &styling::RenderingState,
feature: &TFeature,
point: &geo::Point,
symbol: &styling::Symbol<TFeature>,
)
{
self.stats.points += 1;
let px = point.x();
let py = point.y();
let shape = kurbo::Circle {
center: kurbo::Point { x: px, y: py },
radius: symbol.radius,
};
self.draw_shape(rendering_state, feature, &shape, symbol);
if let Some(icon) = &symbol.icon
{
let size = icon.size as f64;
let (min_x, min_y) = match icon.anchor
{
styling::IconAnchor::Center => (px - size * 0.5, py - size * 0.5),
styling::IconAnchor::TopLeft => (px, py),
styling::IconAnchor::BottomCenter => (px - size * 0.5, py - size),
};
let target_rect = Rect::new(
coord! { x: min_x, y: min_y },
coord! { x: min_x + size, y: min_y + size },
);
let ptr: *const dyn crate::ImageData = icon.image.as_ref();
let image_id = (ptr as *const ()) as usize as u64;
self.draw_image_data(image_id, icon.image.as_ref(), target_rect);
}
}
fn draw_polygon(
&mut self,
rendering_state: &styling::RenderingState,
feature: &TFeature,
poly: &geo::Polygon,
symbol: &crate::Symbol<TFeature>,
)
{
self.stats.polygons += 1;
self.stats.polygon_vertices += poly.exterior().0.len() as u64;
self.stats.polygon_vertices += poly
.interiors()
.iter()
.map(|ring| ring.0.len() as u64)
.sum::<u64>();
let shape = poly.shapify();
self.draw_shape(rendering_state, feature, &shape, symbol);
}
fn draw_line_string(
&mut self,
rendering_state: &styling::RenderingState,
feature: &TFeature,
line: &geo::LineString,
symbol: &crate::Symbol<TFeature>,
)
{
use geo::CoordsIter;
if symbol.stroke_width > 0.0 && line.coords_count() >= 2
{
self.stats.line_strings += 1;
self.stats.line_vertices += line.0.len() as u64;
let shape = line.shapify();
self.scene.stroke(
&kurbo::Stroke::new(symbol.stroke_width),
vello::kurbo::Affine::IDENTITY,
&(symbol.stroke_color)(rendering_state, feature).brushify(),
None,
&shape,
);
}
}
fn draw_image(
&mut self,
_rendering_state: &styling::RenderingState,
_feature: &TFeature,
image: ImageFeature<BoxedImageDataRef<'_>>,
target_rect: Rect,
)
{
self.stats.images += 1;
self.draw_image_data(image.id(), image.image_data(), target_rect);
}
fn draw_label(
&mut self,
rendering_state: &styling::RenderingState,
feature: &TFeature,
anchor: geo::Point,
config: &styling::LabelConfig<TFeature>,
)
{
self.stats.labels_attempted += 1;
if rendering_state.zoom_level < config.min_zoom || !self.can_draw_label_at_anchor(anchor)
{
return;
}
let Some(text) = (config.text)(feature)
else
{
return;
};
if text.trim().is_empty()
{
return;
}
let mut font_context = FontContext::new();
let mut layout_context = LayoutContext::<[u8; 4]>::new();
let mut builder = layout_context.ranged_builder(&mut font_context, &text, 1.0, true);
builder.push_default(StyleProperty::FontSize(config.font_size));
let mut layout: Layout<[u8; 4]> = builder.build(&text);
layout.break_all_lines(None);
let origin_x = anchor.x() as f32 - layout.width() * 0.5;
let origin_y = anchor.y() as f32 - config.font_size - 4.0;
let halo_brush = (config.halo_color)(rendering_state, feature).brushify();
let text_brush = (config.color)(rendering_state, feature).brushify();
let halo_offsets = [
(-Self::LABEL_HALO_RADIUS_PX, 0.0),
(Self::LABEL_HALO_RADIUS_PX, 0.0),
(0.0, -Self::LABEL_HALO_RADIUS_PX),
(0.0, Self::LABEL_HALO_RADIUS_PX),
(-Self::LABEL_HALO_RADIUS_PX, -Self::LABEL_HALO_RADIUS_PX),
(-Self::LABEL_HALO_RADIUS_PX, Self::LABEL_HALO_RADIUS_PX),
(Self::LABEL_HALO_RADIUS_PX, -Self::LABEL_HALO_RADIUS_PX),
(Self::LABEL_HALO_RADIUS_PX, Self::LABEL_HALO_RADIUS_PX),
];
for line in layout.lines()
{
for item in line.items()
{
let PositionedLayoutItem::GlyphRun(glyph_run) = item
else
{
continue;
};
let glyphs: Vec<vello::Glyph> = glyph_run
.positioned_glyphs()
.map(|glyph| vello::Glyph {
id: glyph.id,
x: origin_x + glyph.x,
y: origin_y + glyph.y,
})
.collect();
for (dx, dy) in halo_offsets
{
let halo_glyphs: Vec<vello::Glyph> = glyphs
.iter()
.map(|glyph| vello::Glyph {
id: glyph.id,
x: glyph.x + dx,
y: glyph.y + dy,
})
.collect();
self.draw_glyph_run_with_brush(glyph_run.run(), &halo_glyphs, &halo_brush);
}
self.draw_glyph_run_with_brush(glyph_run.run(), &glyphs, &text_brush);
}
}
self.drawn_label_anchors.push(anchor);
self.stats.labels_drawn += 1;
}
}
#[cfg(test)]
mod tests
{
use super::*;
use geo::CoordsIter;
use std::{sync::Arc, time::Duration};
#[derive(Clone)]
struct TestFeature
{
geometry: geo::Geometry,
}
impl crate::GeometryRef for TestFeature
{
fn geometry_ref(&self) -> &geo::Geometry
{
&self.geometry
}
}
fn base_map() -> Arc<crate::Map<TestFeature>>
{
let polygon = geo::Polygon::new(
geo::LineString::new(vec![
geo::coord! { x: -10.0, y: -10.0 },
geo::coord! { x: 10.0, y: -10.0 },
geo::coord! { x: 10.0, y: 10.0 },
geo::coord! { x: -10.0, y: 10.0 },
geo::coord! { x: -10.0, y: -10.0 },
]),
vec![],
);
crate::MapBuilder::new()
.add_visible_layer(
"base",
crate::FeaturesVecLayer::from(vec![TestFeature {
geometry: polygon.into(),
}]),
)
.into()
}
fn overlay_map() -> Arc<crate::Map<TestFeature>>
{
crate::MapBuilder::new()
.add_visible_layer(
"overlay",
crate::FeaturesVecLayer::from(vec![TestFeature {
geometry: geo::Point::new(0.0, 0.0).into(),
}]),
)
.into()
}
fn base_style() -> styling::Style<TestFeature>
{
styling::StyleBuilder::new()
.set_background_color(styling::Rgba::TRANSPARENT)
.add_rule_for_type(
crate::GeometryType::Polygon,
styling::Symbol {
fill_color: styling::rgb(1.0, 0.0, 0.0).into(),
stroke_width: 0.0,
..Default::default()
},
)
.into()
}
fn overlay_style() -> styling::Style<TestFeature>
{
styling::StyleBuilder::new()
.set_background_color(styling::Rgba::TRANSPARENT)
.add_rule_for_type(
crate::GeometryType::Point,
styling::Symbol {
radius: 8.0,
fill_color: styling::rgb(0.0, 0.0, 1.0).into(),
stroke_width: 0.0,
..Default::default()
},
)
.into()
}
#[test]
fn test_linestring_shapify()
{
let line = geo::LineString::new(vec![
geo::coord! { x: 0.0, y: 0.0 },
geo::coord! { x: 10.0, y: 0.0 },
geo::coord! { x: 10.0, y: 10.0 },
]);
let shape = line.shapify();
let elements: Vec<_> = shape.elements().to_vec();
assert_eq!(elements.len(), 3);
assert!(matches!(elements[0], kurbo::PathEl::MoveTo(_)));
assert!(matches!(elements[1], kurbo::PathEl::LineTo(_)));
assert!(matches!(elements[2], kurbo::PathEl::LineTo(_)));
}
#[test]
fn test_linestring_degenerate()
{
let line = geo::LineString::new(vec![geo::coord! { x: 0.0, y: 0.0 }]);
assert_eq!(line.coords_count(), 1);
}
#[test]
fn test_linestring_closed_ring()
{
let line = geo::LineString::new(vec![
geo::coord! { x: 0.0, y: 0.0 },
geo::coord! { x: 10.0, y: 0.0 },
geo::coord! { x: 10.0, y: 10.0 },
geo::coord! { x: 0.0, y: 10.0 },
geo::coord! { x: 0.0, y: 0.0 },
]);
let shape = line.shapify();
assert_eq!(shape.elements().len(), 5);
}
#[test]
fn test_add_overlay_does_not_panic()
{
let (device, queue) = ThreadedRenderer::new_device().unwrap();
let mut renderer =
ThreadedRenderer::new_thread(device, queue, base_map(), base_style()).unwrap();
renderer
.add_overlay(overlay_map(), overlay_style(), 10)
.unwrap();
renderer.update().unwrap();
}
#[test]
fn test_overlay_renders_on_top_of_base()
{
let (device, queue) = ThreadedRenderer::new_device().unwrap();
let mut renderer =
ThreadedRenderer::new_thread(device, queue, base_map(), base_style()).unwrap();
renderer
.resize(64, 64, Option::<fn(&Texture)>::None)
.unwrap();
let receiver = renderer.subscribe_to_cpu_image().unwrap();
renderer
.add_overlay(overlay_map(), overlay_style(), 10)
.unwrap();
renderer.update_overlay(10).unwrap();
let image = receiver.recv_timeout(Duration::from_secs(60)).unwrap();
let center_x = image.width / 2;
let center_y = image.height / 2;
let pixel_index = ((center_y * image.width + center_x) * 4) as usize;
let red = image.data[pixel_index];
let blue = image.data[pixel_index + 2];
assert!(
blue > red,
"Expected overlay blue pixel to dominate over base red"
);
}
}