use super::{
CROSSHAIR_RGBA, SELECTION_FILL_RGBA, highlight_marker_plot_position,
highlight_mask_plot_position, highlight_mask_rgba,
};
use crate::LineStyle;
use crate::picking::PickingPass;
use crate::transform::data_value_to_plot_with_axis_range;
use crate::{LineType, Size, camera::CameraUniform, grid::Grid, plot_state::PlotState};
use iced::widget::shader::Viewport;
use iced::{Rectangle, wgpu::*};
const MSAA_SAMPLE_COUNT: u32 = 4;
pub struct RenderParams<'a> {
pub encoder: &'a mut CommandEncoder,
pub target: &'a TextureView,
pub clip_bounds: &'a Rectangle<u32>,
}
fn msaa_state() -> MultisampleState {
MultisampleState {
count: MSAA_SAMPLE_COUNT,
mask: !0,
alpha_to_coverage_enabled: false,
}
}
fn create_color_texture(
device: &Device,
label: &'static str,
width: u32,
height: u32,
sample_count: u32,
format: TextureFormat,
usage: TextureUsages,
) -> Texture {
device.create_texture(&TextureDescriptor {
label: Some(label),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count,
dimension: TextureDimension::D2,
format,
usage,
view_formats: &[],
})
}
fn msaa_attachment<'a>(
targets: &'a MsaaTargets,
load: LoadOp<Color>,
) -> RenderPassColorAttachment<'a> {
RenderPassColorAttachment {
view: &targets.msaa_view,
resolve_target: Some(&targets.resolved_view),
ops: Operations {
load,
store: StoreOp::Store,
},
depth_slice: None,
}
}
fn target_attachment(target: &TextureView) -> RenderPassColorAttachment<'_> {
RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: Operations {
load: LoadOp::Load,
store: StoreOp::Store,
},
depth_slice: None,
}
}
#[derive(Default, Clone)]
struct LineSegment {
first_vertex: u32,
vertex_count: u32,
}
struct VertexBuffer {
buffer: Buffer,
vertex_count: u32,
}
struct LineBuffer {
buffer: Buffer,
segments: Vec<LineSegment>,
}
struct LineVertex<'a> {
start: [f32; 2],
end: [f32; 2],
color: &'a iced::Color,
style: u32,
distance_start: f32,
segment_length_world: f32,
param: f32,
width: f32,
width_mode: u32,
along: f32,
side: f32,
}
#[derive(Clone, Copy)]
struct LineRenderStyle {
width: Size,
line_style: u32,
style_param: f32,
}
struct PolylineRef<'a> {
positions: &'a [[f32; 2]],
distances: &'a [f32],
colors: &'a [iced::Color],
}
struct PipelineCache {
marker: Option<RenderPipeline>,
line: Option<RenderPipeline>,
fill: Option<RenderPipeline>,
overlay: Option<RenderPipeline>,
line_overlay: Option<RenderPipeline>,
composite: Option<RenderPipeline>,
}
impl PipelineCache {
fn new() -> Self {
Self {
marker: None,
line: None,
fill: None,
overlay: None,
line_overlay: None,
composite: None,
}
}
}
struct MsaaTargets {
width: u32,
height: u32,
_msaa_texture: Texture,
msaa_view: TextureView,
_resolved_texture: Texture,
resolved_view: TextureView,
composite_bind_group: BindGroup,
}
struct BufferCache {
markers: Option<VertexBuffer>,
fills: Option<VertexBuffer>,
lines: Option<LineBuffer>,
reflines: Option<LineBuffer>,
selection: Option<VertexBuffer>,
highlight: Option<VertexBuffer>,
highlight_markers: Option<VertexBuffer>,
crosshairs: Option<VertexBuffer>,
}
impl BufferCache {
fn new() -> Self {
Self {
markers: None,
fills: None,
lines: None,
reflines: None,
selection: None,
highlight: None,
highlight_markers: None,
crosshairs: None,
}
}
}
struct VersionTracker {
markers: u64,
fills: u64,
lines: u64,
highlight: u64,
render_offset: glam::DVec2,
}
impl VersionTracker {
fn new() -> Self {
Self {
markers: 0,
fills: 0,
lines: 0,
highlight: 0,
render_offset: glam::DVec2::ZERO,
}
}
}
struct VertexWriter {
data: Vec<u8>,
}
impl VertexWriter {
fn new() -> Self {
Self { data: Vec::new() }
}
fn with_capacity(capacity: usize) -> Self {
Self {
data: Vec::with_capacity(capacity),
}
}
fn write_f32(&mut self, value: f32) {
self.data.extend_from_slice(&value.to_le_bytes());
}
fn write_u32(&mut self, value: u32) {
self.data.extend_from_slice(&value.to_le_bytes());
}
fn write_position(&mut self, pos: [f32; 2]) {
self.write_f32(pos[0]);
self.write_f32(pos[1]);
}
fn write_color(&mut self, color: &iced::Color) {
self.write_f32(color.r);
self.write_f32(color.g);
self.write_f32(color.b);
self.write_f32(color.a);
}
fn write_line_vertex(&mut self, vertex: LineVertex<'_>) {
self.write_position(vertex.start);
self.write_position(vertex.end);
self.write_color(vertex.color);
self.write_u32(vertex.style);
self.write_f32(vertex.distance_start);
self.write_f32(vertex.segment_length_world);
self.write_f32(vertex.param);
self.write_f32(vertex.width);
self.write_u32(vertex.width_mode);
self.write_f32(vertex.along);
self.write_f32(vertex.side);
}
fn byte_len(&self) -> usize {
self.data.len()
}
fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn as_slice(&self) -> &[u8] {
&self.data
}
}
pub struct PlotRenderer {
format: TextureFormat,
camera_buffer: Buffer,
camera_bind_group: BindGroup,
camera_bgl: BindGroupLayout,
composite_bgl: BindGroupLayout,
composite_sampler: Sampler,
msaa_targets: Option<MsaaTargets>,
pipelines: PipelineCache,
buffers: BufferCache,
versions: VersionTracker,
grid: Grid,
picking: PickingPass,
scale_factor: f32,
bounds_w: u32,
bounds_h: u32,
bounds: Rectangle,
}
impl PlotRenderer {
pub fn new(device: &Device, _queue: &Queue, format: TextureFormat) -> Self {
let camera_bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("camera_bgl"),
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX_FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let camera_buffer = device.create_buffer(&BufferDescriptor {
label: Some("camera_buffer"),
size: std::mem::size_of::<crate::camera::CameraUniform>() as u64,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let camera_bind_group = device.create_bind_group(&BindGroupDescriptor {
label: Some("camera_bg"),
layout: &camera_bgl,
entries: &[BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
}],
});
let composite_bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("plot composite bgl"),
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
],
});
let composite_sampler = device.create_sampler(&SamplerDescriptor {
label: Some("plot composite sampler"),
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Nearest,
min_filter: FilterMode::Nearest,
mipmap_filter: FilterMode::Nearest,
..SamplerDescriptor::default()
});
Self {
format,
camera_buffer,
camera_bind_group,
camera_bgl,
composite_bgl,
composite_sampler,
msaa_targets: None,
pipelines: PipelineCache::new(),
buffers: BufferCache::new(),
versions: VersionTracker::new(),
grid: Grid::default(),
picking: PickingPass::default(),
bounds_w: 0,
bounds_h: 0,
bounds: Rectangle::default(),
scale_factor: 1.0,
}
}
fn screen_to_clip(&self, x: f32, y: f32) -> [f32; 2] {
let w = self.bounds_w.max(1) as f32;
let h = self.bounds_h.max(1) as f32;
[(x / w) * 2.0 - 1.0, 1.0 - (y / h) * 2.0]
}
fn world_to_ndc(&self, world: [f64; 2], camera: &crate::camera::Camera) -> [f32; 2] {
let render_pos = [
(world[0] - camera.render_offset.x) as f32,
(world[1] - camera.render_offset.y) as f32,
];
let ndc_x =
(render_pos[0] - camera.effective_position().x as f32) / camera.half_extents.x as f32;
let ndc_y =
(render_pos[1] - camera.effective_position().y as f32) / camera.half_extents.y as f32;
[ndc_x, ndc_y]
}
fn pixels_to_clip_delta(&self, pixels: f32) -> (f32, f32) {
let w = self.bounds_w.max(1) as f32;
let h = self.bounds_h.max(1) as f32;
(2.0 * pixels / w, 2.0 * pixels / h)
}
fn world_to_render_pos(&self, world: [f64; 2], camera: &crate::camera::Camera) -> [f32; 2] {
[
(world[0] - camera.render_offset.x) as f32,
(world[1] - camera.render_offset.y) as f32,
]
}
fn world_per_pixel(&self, camera: &crate::camera::Camera) -> [f32; 2] {
[
((2.0 * camera.half_extents.x) / self.bounds_w.max(1) as f64) as f32,
((2.0 * camera.half_extents.y) / self.bounds_h.max(1) as f64) as f32,
]
}
fn ensure_pipelines_and_update_grid(
&mut self,
device: &Device,
_queue: &Queue,
state: &PlotState,
) {
self.ensure_marker_pipeline(device);
self.grid
.ensure_pipeline(device, self.format, &self.camera_bgl, MSAA_SAMPLE_COUNT);
self.grid.update(device, state);
if !state.fills.is_empty() {
self.ensure_fill_pipeline(device);
}
if state.series.iter().any(|s| s.line_style.is_some())
|| !state.vlines.is_empty()
|| !state.hlines.is_empty()
{
self.ensure_line_pipeline(device);
}
self.ensure_overlay_pipeline(device);
self.ensure_line_overlay_pipeline(device);
self.ensure_composite_pipeline(device);
}
fn set_bounds(&mut self, w: u32, h: u32) {
self.bounds_w = w;
self.bounds_h = h;
}
fn set_scale_factor(&mut self, scale: f32) {
self.scale_factor = scale;
}
fn ensure_msaa_targets(&mut self, device: &Device, width: u32, height: u32) {
let width = width.max(1);
let height = height.max(1);
if self
.msaa_targets
.as_ref()
.is_some_and(|targets| targets.width == width && targets.height == height)
{
return;
}
let msaa_texture = create_color_texture(
device,
"iced_plot msaa color",
width,
height,
MSAA_SAMPLE_COUNT,
self.format,
TextureUsages::RENDER_ATTACHMENT,
);
let msaa_view = msaa_texture.create_view(&TextureViewDescriptor::default());
let resolved_texture = create_color_texture(
device,
"iced_plot resolved color",
width,
height,
1,
self.format,
TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
);
let resolved_view = resolved_texture.create_view(&TextureViewDescriptor::default());
let composite_bind_group = device.create_bind_group(&BindGroupDescriptor {
label: Some("plot composite bind group"),
layout: &self.composite_bgl,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&resolved_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&self.composite_sampler),
},
],
});
self.msaa_targets = Some(MsaaTargets {
width,
height,
_msaa_texture: msaa_texture,
msaa_view,
_resolved_texture: resolved_texture,
resolved_view,
composite_bind_group,
});
}
fn sync(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
let offset_changed = self.versions.render_offset != state.camera.render_offset;
if state.markers_version != self.versions.markers || offset_changed {
self.rebuild_markers(device, queue, state);
self.versions.markers = state.markers_version;
}
if state.fills_version != self.versions.fills || offset_changed {
self.rebuild_fills(device, queue, state);
self.versions.fills = state.fills_version;
}
if state.lines_version != self.versions.lines || offset_changed {
self.rebuild_lines(device, queue, state);
self.versions.lines = state.lines_version;
}
self.rebuild_reflines(device, queue, state);
self.versions.render_offset = state.camera.render_offset;
self.rebuild_selection(device, queue, state);
if state.highlight_version != self.versions.highlight || offset_changed {
self.rebuild_highlight(device, queue, state);
self.versions.highlight = state.highlight_version;
}
self.rebuild_crosshairs(device, queue, state);
}
pub(crate) fn prepare_frame(
&mut self,
device: &Device,
queue: &Queue,
viewport: &Viewport,
bounds: &Rectangle,
state: &PlotState,
) {
self.bounds = *bounds;
let scale_factor = viewport.scale_factor();
let bounds_width = (bounds.width * scale_factor) as u32;
let bounds_height = (bounds.height * scale_factor) as u32;
let viewport_size = viewport.physical_size();
self.set_bounds(bounds_width, bounds_height);
self.set_scale_factor(scale_factor);
self.ensure_msaa_targets(device, viewport_size.width, viewport_size.height);
self.picking
.set_view(bounds_width, bounds_height, scale_factor);
self.ensure_pipelines_and_update_grid(device, queue, state);
let mut cam_u = CameraUniform::default();
cam_u.update(&state.camera, bounds_width, bounds_height);
queue.write_buffer(&self.camera_buffer, 0, bytemuck::bytes_of(&cam_u));
self.sync(device, queue, state);
}
pub(crate) fn service_picking(
&mut self,
instance_id: u64,
device: &Device,
queue: &Queue,
state: &PlotState,
) {
let marker_buffer = self.buffers.markers.as_ref().map(|vb| &vb.buffer);
let marker_instances = self
.buffers
.markers
.as_ref()
.map(|vb| vb.vertex_count)
.unwrap_or(0);
self.picking.service(
instance_id,
device,
queue,
&self.camera_bind_group,
&self.camera_bgl,
marker_buffer,
marker_instances,
&state.points,
&state.series,
);
}
pub fn ensure_marker_pipeline(&mut self, device: &Device) {
if self.pipelines.marker.is_some() {
return;
}
let shader = device.create_shader_module(include_wgsl!("../shaders/markers.wgsl"));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("markers layout"),
bind_group_layouts: &[&self.camera_bgl],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("markers pipeline"),
layout: Some(&layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: PipelineCompilationOptions::default(),
buffers: &[VertexBufferLayout {
array_stride: 36u64,
step_mode: VertexStepMode::Instance,
attributes: &[
VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2,
},
VertexAttribute {
offset: std::mem::size_of::<[f32; 2]>() as u64,
shader_location: 1,
format: VertexFormat::Float32x4,
},
VertexAttribute {
offset: std::mem::size_of::<[f32; 6]>() as u64,
shader_location: 2,
format: VertexFormat::Uint32,
},
VertexAttribute {
offset: std::mem::size_of::<[f32; 6]>() as u64
+ std::mem::size_of::<u32>() as u64,
shader_location: 3,
format: VertexFormat::Float32,
},
VertexAttribute {
offset: std::mem::size_of::<[f32; 7]>() as u64
+ std::mem::size_of::<u32>() as u64,
shader_location: 4,
format: VertexFormat::Uint32,
},
],
}],
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(ColorTargetState {
format: self.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: msaa_state(),
multiview: None,
cache: None,
});
self.pipelines.marker = Some(pipeline);
}
pub fn ensure_line_pipeline(&mut self, device: &Device) {
if self.pipelines.line.is_some() {
return;
}
let shader = device.create_shader_module(include_wgsl!("../shaders/line.wgsl"));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("line layout"),
bind_group_layouts: &[&self.camera_bgl],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("line pipeline"),
layout: Some(&layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: PipelineCompilationOptions::default(),
buffers: &[VertexBufferLayout {
array_stride: 64,
step_mode: VertexStepMode::Vertex,
attributes: &[
VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2, },
VertexAttribute {
offset: 8,
shader_location: 1,
format: VertexFormat::Float32x2, },
VertexAttribute {
offset: 16,
shader_location: 2,
format: VertexFormat::Float32x4, },
VertexAttribute {
offset: 32,
shader_location: 3,
format: VertexFormat::Uint32, },
VertexAttribute {
offset: 36,
shader_location: 4,
format: VertexFormat::Float32, },
VertexAttribute {
offset: 40,
shader_location: 5,
format: VertexFormat::Float32, },
VertexAttribute {
offset: 44,
shader_location: 6,
format: VertexFormat::Float32, },
VertexAttribute {
offset: 48,
shader_location: 7,
format: VertexFormat::Float32, },
VertexAttribute {
offset: 52,
shader_location: 8,
format: VertexFormat::Uint32, },
VertexAttribute {
offset: 56,
shader_location: 9,
format: VertexFormat::Float32, },
VertexAttribute {
offset: 60,
shader_location: 10,
format: VertexFormat::Float32, },
],
}],
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(ColorTargetState {
format: self.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: msaa_state(),
multiview: None,
cache: None,
});
self.pipelines.line = Some(pipeline);
}
pub fn ensure_fill_pipeline(&mut self, device: &Device) {
if self.pipelines.fill.is_some() {
return;
}
let shader = device.create_shader_module(include_wgsl!("../shaders/fill.wgsl"));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("fill layout"),
bind_group_layouts: &[&self.camera_bgl],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("fill pipeline"),
layout: Some(&layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: PipelineCompilationOptions::default(),
buffers: &[VertexBufferLayout {
array_stride: 24,
step_mode: VertexStepMode::Vertex,
attributes: &[
VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2,
},
VertexAttribute {
offset: 8,
shader_location: 1,
format: VertexFormat::Float32x4,
},
],
}],
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(ColorTargetState {
format: self.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: msaa_state(),
multiview: None,
cache: None,
});
self.pipelines.fill = Some(pipeline);
}
pub fn ensure_overlay_pipeline(&mut self, device: &Device) {
if self.pipelines.overlay.is_some() {
return;
}
let shader = device.create_shader_module(include_wgsl!("../shaders/selection.wgsl"));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("overlay layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("overlay pipeline"),
layout: Some(&layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: PipelineCompilationOptions::default(),
buffers: &[VertexBufferLayout {
array_stride: (std::mem::size_of::<[f32; 2]>()
+ std::mem::size_of::<[f32; 4]>()) as u64,
step_mode: VertexStepMode::Vertex,
attributes: &[
VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2,
},
VertexAttribute {
offset: std::mem::size_of::<[f32; 2]>() as u64,
shader_location: 1,
format: VertexFormat::Float32x4,
},
],
}],
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(ColorTargetState {
format: self.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: msaa_state(),
multiview: None,
cache: None,
});
self.pipelines.overlay = Some(pipeline);
}
pub fn ensure_line_overlay_pipeline(&mut self, device: &Device) {
if self.pipelines.line_overlay.is_some() {
return;
}
let shader = device.create_shader_module(include_wgsl!("../shaders/selection.wgsl"));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("line overlay layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("line overlay pipeline"),
layout: Some(&layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: PipelineCompilationOptions::default(),
buffers: &[VertexBufferLayout {
array_stride: 24,
step_mode: VertexStepMode::Vertex,
attributes: &[
VertexAttribute {
format: VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 8,
shader_location: 1,
},
],
}],
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(ColorTargetState {
format: self.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::LineList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: msaa_state(),
multiview: None,
cache: None,
});
self.pipelines.line_overlay = Some(pipeline);
}
fn ensure_composite_pipeline(&mut self, device: &Device) {
if self.pipelines.composite.is_some() {
return;
}
let shader = device.create_shader_module(include_wgsl!("../shaders/composite.wgsl"));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("plot composite layout"),
bind_group_layouts: &[&self.composite_bgl],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("plot composite pipeline"),
layout: Some(&layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: PipelineCompilationOptions::default(),
buffers: &[],
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(ColorTargetState {
format: self.format,
blend: Some(BlendState {
color: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
alpha: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
}),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: MultisampleState::default(),
multiview: None,
cache: None,
});
self.pipelines.composite = Some(pipeline);
}
fn rebuild_markers(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
let marker_series_count: usize = state
.series
.iter()
.filter(|s| s.marker != u32::MAX)
.map(|s| s.len)
.sum();
if marker_series_count == 0 {
self.buffers.markers = None;
return;
}
let mut writer = VertexWriter::with_capacity(marker_series_count * 36);
let mut id_map: Vec<(u32, u32)> = Vec::with_capacity(marker_series_count);
for (span_idx, s) in state.series.iter().enumerate() {
if s.marker == u32::MAX {
continue;
}
let end = s.start + s.len;
if s.len == 0 || end > state.points.len() {
continue;
}
for (local_i, p) in state.points[s.start..end].iter().enumerate() {
let render_pos = self.world_to_render_pos(p.position, &state.camera);
let color_idx = s.start + local_i;
let color = state.point_colors.get(color_idx).unwrap_or(&s.color);
writer.write_position(render_pos);
writer.write_color(color);
writer.write_u32(s.marker);
writer.write_f32(p.size);
writer.write_u32(crate::point::marker_flags(p.size_mode, s.pickable));
let original_idx = s.point_indices.get(local_i).copied().unwrap_or(local_i);
id_map.push((span_idx as u32, original_idx as u32));
}
}
let data = writer.as_slice();
let needed = data.len() as u64;
let recreate = match &self.buffers.markers {
Some(vb) => vb.buffer.size() < needed,
None => true,
};
if recreate {
self.buffers.markers = Some(VertexBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("marker vb"),
size: needed.max(2048),
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
vertex_count: marker_series_count as u32,
});
} else if let Some(vb) = &mut self.buffers.markers {
vb.vertex_count = marker_series_count as u32;
}
if let Some(vb) = &self.buffers.markers {
queue.write_buffer(&vb.buffer, 0, data);
}
self.picking.set_id_map(id_map);
}
fn rebuild_fills(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
self.buffers.fills = None;
if state.fills.is_empty() {
return;
}
let mut writer = VertexWriter::new();
for fill in state.fills.iter() {
for world_pos in fill.vertices.iter() {
let render_pos = self.world_to_render_pos(*world_pos, &state.camera);
writer.write_position(render_pos);
writer.write_color(&fill.color);
}
}
if writer.is_empty() {
return;
}
let data = writer.as_slice();
self.buffers.fills = Some(VertexBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("fill vb"),
size: data.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
vertex_count: (data.len() / 24) as u32,
});
if let Some(vb) = &self.buffers.fills {
queue.write_buffer(&vb.buffer, 0, data);
}
}
fn rebuild_lines(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
self.buffers.lines = None;
if state.series.iter().all(|s| s.line_style.is_none()) {
return;
}
let mut writer = VertexWriter::new();
let mut segs: Vec<LineSegment> = Vec::new();
for s in state.series.iter() {
let Some(line_style) = s.line_style else {
continue;
};
if s.len < 2 {
continue;
}
let (line_style_u32, style_param) = line_style_params(line_style);
let render_style = LineRenderStyle {
width: line_style.width,
line_style: line_style_u32,
style_param,
};
let points_slice = &state.points[s.start..s.start + s.len];
let mut poly_positions: Vec<[f32; 2]> = Vec::new();
let mut poly_distances: Vec<f32> = Vec::new();
let mut poly_colors: Vec<iced::Color> = Vec::new();
let mut cumulative_distance = 0.0f32;
for (i, point) in points_slice.iter().enumerate() {
let break_segment = i > 0
&& s.point_indices
.get(i)
.zip(s.point_indices.get(i - 1))
.is_some_and(|(curr, prev)| *curr != *prev + 1);
if break_segment {
write_polyline_triangles(
&mut writer,
&mut segs,
PolylineRef {
positions: &poly_positions,
distances: &poly_distances,
colors: &poly_colors,
},
render_style,
);
poly_positions.clear();
poly_distances.clear();
poly_colors.clear();
cumulative_distance = 0.0;
}
let render_pos = self.world_to_render_pos(point.position, &state.camera);
let color = *state.point_colors.get(s.start + i).unwrap_or(&s.color);
if let Some(last_pos) = poly_positions.last() {
let dx = render_pos[0] - last_pos[0];
let dy = render_pos[1] - last_pos[1];
let segment_length = (dx * dx + dy * dy).sqrt();
if segment_length <= f32::EPSILON {
if let Some(last_color) = poly_colors.last_mut() {
*last_color = color;
}
continue;
}
cumulative_distance += segment_length;
}
poly_positions.push(render_pos);
poly_distances.push(cumulative_distance);
poly_colors.push(color);
}
write_polyline_triangles(
&mut writer,
&mut segs,
PolylineRef {
positions: &poly_positions,
distances: &poly_distances,
colors: &poly_colors,
},
render_style,
);
}
if writer.is_empty() {
return;
}
let data = writer.as_slice();
self.buffers.lines = Some(LineBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("line vb"),
size: data.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
segments: segs,
});
if let Some(lb) = &self.buffers.lines {
queue.write_buffer(&lb.buffer, 0, data);
}
}
fn rebuild_reflines(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
self.buffers.reflines = None;
if state.vlines.is_empty() && state.hlines.is_empty() {
return;
}
let mut writer = VertexWriter::new();
let mut segs: Vec<LineSegment> = Vec::new();
let world_per_px = self.world_per_pixel(&state.camera);
let cam = &state.camera;
let left = cam.position.x - cam.half_extents.x;
let right = cam.position.x + cam.half_extents.x;
let bottom = cam.position.y - cam.half_extents.y;
let top = cam.position.y + cam.half_extents.y;
for vline in state.vlines.iter() {
let Some(vx_plot) = data_value_to_plot_with_axis_range(
vline.x,
state.x_axis_scale,
vline.transform.as_ref(),
Some(state.camera.x_range()),
) else {
continue;
};
let half_width = reference_line_half_extent(vline.line_style.width, true, world_per_px);
if vx_plot + (half_width as f64) < left || vx_plot - (half_width as f64) > right {
continue;
}
let (line_style_u32, style_param) = line_style_params(vline.line_style);
let render_style = LineRenderStyle {
width: vline.line_style.width,
line_style: line_style_u32,
style_param,
};
let positions = [
self.world_to_render_pos([vx_plot, bottom], &state.camera),
self.world_to_render_pos([vx_plot, top], &state.camera),
];
let distances = [0.0, (top - bottom) as f32];
let colors = [vline.color, vline.color];
write_polyline_triangles(
&mut writer,
&mut segs,
PolylineRef {
positions: &positions,
distances: &distances,
colors: &colors,
},
render_style,
);
}
for hline in state.hlines.iter() {
let Some(hy_plot) = data_value_to_plot_with_axis_range(
hline.y,
state.y_axis_scale,
hline.transform.as_ref(),
Some(state.camera.y_range()),
) else {
continue;
};
let half_width =
reference_line_half_extent(hline.line_style.width, false, world_per_px);
if hy_plot + (half_width as f64) < bottom || hy_plot - (half_width as f64) > top {
continue;
}
let (line_style_u32, style_param) = line_style_params(hline.line_style);
let render_style = LineRenderStyle {
width: hline.line_style.width,
line_style: line_style_u32,
style_param,
};
let positions = [
self.world_to_render_pos([left, hy_plot], &state.camera),
self.world_to_render_pos([right, hy_plot], &state.camera),
];
let distances = [0.0, (right - left) as f32];
let colors = [hline.color, hline.color];
write_polyline_triangles(
&mut writer,
&mut segs,
PolylineRef {
positions: &positions,
distances: &distances,
colors: &colors,
},
render_style,
);
}
if writer.is_empty() {
return;
}
let data = writer.as_slice();
self.buffers.reflines = Some(LineBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("refline vb"),
size: data.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
segments: segs,
});
if let Some(lb) = &self.buffers.reflines {
queue.write_buffer(&lb.buffer, 0, data);
}
}
fn rebuild_selection(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
let w = self.bounds_w.max(1) as f32;
let h = self.bounds_h.max(1) as f32;
if w <= 1.0 || h <= 1.0 {
return;
}
if state.selection.active || state.selection.moved {
let p0 = state.selection.start * self.scale_factor;
let p1 = state.selection.end * self.scale_factor;
let min_x = p0.x.min(p1.x);
let max_x = p0.x.max(p1.x);
let min_y = p0.y.min(p1.y);
let max_y = p0.y.max(p1.y);
let tl = self.screen_to_clip(min_x, min_y);
let br = self.screen_to_clip(max_x, max_y);
let tr = [br[0], tl[1]];
let bl = [tl[0], br[1]];
let mut data: Vec<f32> = Vec::new();
for v in [tl, tr, bl, br] {
data.extend_from_slice(&v);
data.extend_from_slice(&SELECTION_FILL_RGBA);
}
let raw = bytemuck::cast_slice(&data);
self.buffers.selection = Some(VertexBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("selection vb"),
size: raw.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
vertex_count: 4,
});
if let Some(vb) = &self.buffers.selection {
queue.write_buffer(&vb.buffer, 0, raw);
}
} else {
self.buffers.selection = None;
}
}
fn rebuild_highlight(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
self.buffers.highlight = None;
self.buffers.highlight_markers = None;
if state.highlighted_points.is_empty() {
return;
}
let w = self.bounds_w.max(1) as f32;
let h = self.bounds_h.max(1) as f32;
if w <= 1.0 || h <= 1.0 {
return;
}
let mut mask_box_data: Vec<f32> = Vec::new();
let mut marker_writer = VertexWriter::new();
for highlight_point in state.highlighted_points.iter() {
if let Some(mask_padding) = highlight_point.mask_padding
&& let Some(marker_style) = highlight_point.marker_style
{
let Some(world_pos) = highlight_mask_plot_position(highlight_point, state) else {
continue;
};
let ndc = self.world_to_ndc(world_pos, &state.camera);
let mask_box_size_px =
marker_style.size.to_px(&state.camera, &state.bounds) + mask_padding;
let (dx, dy) = self.pixels_to_clip_delta(mask_box_size_px.max(1.0));
let tl = [ndc[0] - dx, ndc[1] + dy];
let tr = [ndc[0] + dx, ndc[1] + dy];
let bl = [ndc[0] - dx, ndc[1] - dy];
let br = [ndc[0] + dx, ndc[1] - dy];
let color = highlight_mask_rgba(highlight_point.color);
for v in [tl, tr, bl, br] {
mask_box_data.extend_from_slice(&v);
mask_box_data.extend_from_slice(&color);
}
}
if let Some(marker_style) = highlight_point.marker_style {
let Some(plot_pos) = highlight_marker_plot_position(highlight_point, state) else {
continue;
};
let render_pos = self.world_to_render_pos(plot_pos, &state.camera);
let (size, size_mode) = marker_style.size.to_raw();
marker_writer.write_position(render_pos);
marker_writer.write_color(&highlight_point.color);
marker_writer.write_u32(marker_style.marker_type as u32);
marker_writer.write_f32(size);
marker_writer.write_u32(crate::point::marker_flags(size_mode, true));
}
}
if !mask_box_data.is_empty() {
let raw = bytemuck::cast_slice(&mask_box_data);
self.buffers.highlight = Some(VertexBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("highlight mask boxes vb"),
size: raw.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
vertex_count: (mask_box_data.len() / 6) as u32, });
if let Some(vb) = &self.buffers.highlight {
queue.write_buffer(&vb.buffer, 0, raw);
}
}
if !marker_writer.is_empty() {
let data = marker_writer.as_slice();
let marker_count = (data.len() / 36) as u32; self.buffers.highlight_markers = Some(VertexBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("highlight markers vb"),
size: data.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
vertex_count: marker_count,
});
if let Some(vb) = &self.buffers.highlight_markers {
queue.write_buffer(&vb.buffer, 0, data);
}
}
}
fn rebuild_crosshairs(&mut self, device: &Device, queue: &Queue, state: &PlotState) {
self.buffers.crosshairs = None;
if !state.crosshairs_enabled {
return;
}
let w = self.bounds_w.max(1) as f32;
let h = self.bounds_h.max(1) as f32;
if w <= 1.0 || h <= 1.0 {
return;
}
let pos = state.crosshairs_position * self.scale_factor;
if pos.x < 0.0 || pos.y < 0.0 || pos.x > w || pos.y > h {
return;
}
let cursor_clip = self.screen_to_clip(pos.x, pos.y);
let mut data: Vec<f32> = Vec::new();
let left = [-1.0, cursor_clip[1]];
let right = [1.0, cursor_clip[1]];
let top = [cursor_clip[0], 1.0];
let bottom = [cursor_clip[0], -1.0];
data.extend_from_slice(&left);
data.extend_from_slice(&CROSSHAIR_RGBA);
data.extend_from_slice(&right);
data.extend_from_slice(&CROSSHAIR_RGBA);
data.extend_from_slice(&top);
data.extend_from_slice(&CROSSHAIR_RGBA);
data.extend_from_slice(&bottom);
data.extend_from_slice(&CROSSHAIR_RGBA);
let raw = bytemuck::cast_slice(&data);
self.buffers.crosshairs = Some(VertexBuffer {
buffer: device.create_buffer(&BufferDescriptor {
label: Some("crosshairs vb"),
size: raw.len() as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
vertex_count: 4,
});
if let Some(vb) = &self.buffers.crosshairs {
queue.write_buffer(&vb.buffer, 0, raw);
}
}
pub fn encode(&self, params: RenderParams) {
let Some(msaa_targets) = &self.msaa_targets else {
return;
};
let x = self.bounds.x * self.scale_factor;
let y = self.bounds.y * self.scale_factor;
let width = self.bounds.width * self.scale_factor;
let height = self.bounds.height * self.scale_factor;
{
let mut pass = params.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("iced_plot main"),
color_attachments: &[Some(msaa_attachment(
msaa_targets,
LoadOp::Clear(Color::TRANSPARENT),
))],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
pass.set_viewport(x, y, width, height, 0.0, 1.0);
pass.set_scissor_rect(
params.clip_bounds.x,
params.clip_bounds.y,
params.clip_bounds.width,
params.clip_bounds.height,
);
self.grid.draw(&mut pass, &self.camera_bind_group);
if let (Some(pipeline), Some(vb)) = (self.pipelines.fill.as_ref(), &self.buffers.fills)
{
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.camera_bind_group, &[]);
pass.set_vertex_buffer(0, vb.buffer.slice(..));
pass.draw(0..vb.vertex_count, 0..1);
}
if let (Some(pipeline), Some(lb)) = (self.pipelines.line.as_ref(), &self.buffers.lines)
{
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.camera_bind_group, &[]);
pass.set_vertex_buffer(0, lb.buffer.slice(..));
for seg in &lb.segments {
pass.draw(seg.first_vertex..seg.first_vertex + seg.vertex_count, 0..1);
}
}
if let (Some(pipeline), Some(lb)) =
(self.pipelines.line.as_ref(), &self.buffers.reflines)
{
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.camera_bind_group, &[]);
pass.set_vertex_buffer(0, lb.buffer.slice(..));
for seg in &lb.segments {
pass.draw(seg.first_vertex..seg.first_vertex + seg.vertex_count, 0..1);
}
}
if let (Some(pipeline), Some(vb)) =
(self.pipelines.marker.as_ref(), &self.buffers.markers)
{
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.camera_bind_group, &[]);
pass.set_vertex_buffer(0, vb.buffer.slice(..));
pass.draw(0..4, 0..vb.vertex_count);
}
if let (Some(pipeline), Some(vb)) = (
self.pipelines.marker.as_ref(),
&self.buffers.highlight_markers,
) {
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.camera_bind_group, &[]);
pass.set_vertex_buffer(0, vb.buffer.slice(..));
pass.draw(0..4, 0..vb.vertex_count);
}
}
if let Some(pipeline) = self.pipelines.overlay.as_ref() {
let mut pass = params.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("selection overlay"),
color_attachments: &[Some(msaa_attachment(msaa_targets, LoadOp::Load))],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
pass.set_viewport(x, y, width, height, 0.0, 1.0);
pass.set_scissor_rect(
params.clip_bounds.x,
params.clip_bounds.y,
params.clip_bounds.width,
params.clip_bounds.height,
);
pass.set_pipeline(pipeline);
if let Some(vb) = &self.buffers.selection {
pass.set_vertex_buffer(0, vb.buffer.slice(..));
pass.draw(0..vb.vertex_count, 0..1);
}
if let Some(vb) = &self.buffers.highlight {
pass.set_vertex_buffer(0, vb.buffer.slice(..));
let quad_count = vb.vertex_count / 4;
for i in 0..quad_count {
pass.draw(i * 4..(i + 1) * 4, 0..1);
}
}
}
if let (Some(pipeline), Some(vb)) = (
self.pipelines.line_overlay.as_ref(),
&self.buffers.crosshairs,
) {
let mut pass = params.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("crosshairs overlay"),
color_attachments: &[Some(msaa_attachment(msaa_targets, LoadOp::Load))],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
pass.set_viewport(x, y, width, height, 0.0, 1.0);
pass.set_scissor_rect(
params.clip_bounds.x,
params.clip_bounds.y,
params.clip_bounds.width,
params.clip_bounds.height,
);
pass.set_pipeline(pipeline);
pass.set_vertex_buffer(0, vb.buffer.slice(..));
pass.draw(0..vb.vertex_count, 0..1);
}
if let Some(pipeline) = self.pipelines.composite.as_ref() {
let mut pass = params.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("iced_plot composite"),
color_attachments: &[Some(target_attachment(params.target))],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
pass.set_viewport(
0.0,
0.0,
msaa_targets.width as f32,
msaa_targets.height as f32,
0.0,
1.0,
);
pass.set_scissor_rect(
params.clip_bounds.x,
params.clip_bounds.y,
params.clip_bounds.width,
params.clip_bounds.height,
);
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &msaa_targets.composite_bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
}
fn line_style_params(style: LineStyle) -> (u32, f32) {
match style.line_type {
LineType::Solid => (0u32, 0.0f32),
LineType::Dotted { spacing } => (1u32, spacing),
LineType::Dashed { length } => (2u32, length),
}
}
fn write_polyline_triangles(
writer: &mut VertexWriter,
segs: &mut Vec<LineSegment>,
polyline: PolylineRef<'_>,
style: LineRenderStyle,
) {
if polyline.positions.len() < 2
|| polyline.positions.len() != polyline.distances.len()
|| polyline.positions.len() != polyline.colors.len()
{
return;
}
let (width, width_mode) = line_width_params(style.width);
let first_vertex = (writer.byte_len() / 64) as u32;
for index in 0..polyline.positions.len() - 1 {
let start = polyline.positions[index];
let end = polyline.positions[index + 1];
let segment_length_world = polyline.distances[index + 1] - polyline.distances[index];
if segment_length_world <= f32::EPSILON {
continue;
}
let start_color = &polyline.colors[index];
let end_color = &polyline.colors[index + 1];
let distance_start = polyline.distances[index];
for (color, along, side) in [
(start_color, 0.0, 1.0),
(start_color, 0.0, -1.0),
(end_color, 1.0, 1.0),
(start_color, 0.0, -1.0),
(end_color, 1.0, 1.0),
(end_color, 1.0, -1.0),
] {
writer.write_line_vertex(LineVertex {
start,
end,
color,
style: style.line_style,
distance_start,
segment_length_world,
param: style.style_param,
width,
width_mode,
along,
side,
});
}
}
let vertex_count = (writer.byte_len() / 64) as u32 - first_vertex;
if vertex_count > 0 {
segs.push(LineSegment {
first_vertex,
vertex_count,
});
}
}
fn reference_line_half_extent(width: Size, vertical: bool, world_per_px: [f32; 2]) -> f32 {
match width {
Size::Pixels(size) => {
let axis_scale = if vertical {
world_per_px[0]
} else {
world_per_px[1]
};
size.max(0.5) * axis_scale * 0.5
}
Size::World(size) => size.max(f64::EPSILON) as f32 * 0.5,
}
}
fn line_width_params(width: Size) -> (f32, u32) {
match width {
Size::Pixels(size) => (size.max(0.5), crate::point::MARKER_SIZE_PIXELS),
Size::World(size) => (
size.max(f64::EPSILON) as f32,
crate::point::MARKER_SIZE_WORLD,
),
}
}